package gdsc.smlm.ij.plugins;
/*-----------------------------------------------------------------------------
* 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.smlm.engine.FitEngineConfiguration;
import gdsc.smlm.fitting.FitConfiguration;
import gdsc.smlm.ij.results.ResultsFileFormat;
import gdsc.smlm.ij.results.ResultsImage;
import gdsc.smlm.ij.results.ResultsTable;
import gdsc.smlm.ij.settings.BatchRun;
import gdsc.smlm.ij.settings.BatchSettings;
import gdsc.smlm.ij.settings.ParameterSettings;
import gdsc.smlm.ij.settings.ResultsSettings;
import gdsc.core.ij.Utils;
import ij.IJ;
import ij.ImagePlus;
import ij.gui.GenericDialog;
import ij.io.OpenDialog;
import ij.plugin.PlugIn;
import java.awt.Checkbox;
import java.awt.TextField;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.XStreamException;
import com.thoughtworks.xstream.io.xml.DomDriver;
/**
* Runs the Peak Fit plugin in a batch.
* <p>
* The batch specifies the set of images to process. For each image the batch can specify a set of values for each of
* the fitting parameters. The Peak Fit plugin is then run for each combination of parameters and the results of each
* run saved to file.
*/
public class BatchPeakFit implements PlugIn, ItemListener, MouseListener
{
private static final String TITLE = "Batch Peak Fit";
private static String configFilename = "";
private XStream xs;
private TextField configFilenameText;
/**
* Default constructor
*/
public BatchPeakFit()
{
xs = createXStream();
}
/*
* (non-Javadoc)
*
* @see ij.plugin.PlugIn#run(java.lang.String)
*/
public void run(String arg)
{
SMLMUsageTracker.recordPlugin(this.getClass(), arg);
if (!showDialog())
return;
runBatch(configFilename);
}
/**
* Reads the batch configuration file. For each parameter variation, create a fitting configuration. Then run the
* fit engine on each input image using each configuration.
*
* @param configurationFilename
*/
private void runBatch(String configurationFilename)
{
BatchSettings settings = loadSettings(configurationFilename);
if (settings == null || settings.parameters.isEmpty())
{
IJ.log("No settings for the fitting engine");
return;
}
if (!new File(settings.resultsDirectory).exists())
{
if (!new File(settings.resultsDirectory).mkdirs())
{
IJ.log("Unable to create the results directory: " + settings.resultsDirectory);
return;
}
}
Document doc = getDefaultSettingsXmlDocument();
if (doc == null)
return;
// Create XML for each variation
ArrayList<String> xmlSettings = new ArrayList<String>();
setParameters(settings.parameters, 0, doc, xmlSettings);
// Run all the variants on the input images
for (String imageFilename : settings.images)
{
ImagePlus imp = IJ.openImage(imageFilename);
if (imp == null)
{
IJ.log("Unable to load image: " + imageFilename);
continue;
}
processImage(settings, imp, xmlSettings);
}
}
private Document getDefaultSettingsXmlDocument()
{
// Create an XML document of the default settings
Document doc = null;
try
{
String configXml = xs.toXML(new FitEngineConfiguration(new FitConfiguration()));
doc = loadDocument(configXml);
}
catch (XStreamException ex)
{
ex.printStackTrace();
}
return doc;
}
/**
* Modify the XML document using the specified values for the given parameter. For each value
* call the method recursively for the next parameter. If there are no more parameters
* then add the XML document to the xmlSettings.
*
* @param parameters
* The list of parameters
* @param i
* Parameter to process
* @param doc
* The XML document containing all the current parameters
* @param xmlSettings
* A list of XML parameter settings
*/
private void setParameters(ArrayList<ParameterSettings> parameters, int i, Document doc,
ArrayList<String> xmlSettings)
{
if (i < parameters.size())
{
ParameterSettings param = parameters.get(i);
NodeList nodes = doc.getElementsByTagName(param.name);
if (nodes.getLength() == 1)
{
// For each value, set the parameter and move to the next
String[] values = param.value.split(",");
for (String value : values)
{
Node node = nodes.item(0);
node.setTextContent(value);
setParameters(parameters, i + 1, doc, xmlSettings);
}
}
else
{
// Just move to the next parameter
setParameters(parameters, i + 1, doc, xmlSettings);
}
}
else
{
// Add the final XML to the parameters to run
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer;
try
{
transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(doc), new StreamResult(writer));
xmlSettings.add(writer.getBuffer().toString());
}
catch (TransformerConfigurationException e)
{
e.printStackTrace();
}
catch (TransformerException e)
{
e.printStackTrace();
}
}
}
private BatchSettings loadSettings(String configurationFilename)
{
BatchSettings settings = null;
FileInputStream fs = null;
try
{
fs = new FileInputStream(configurationFilename);
settings = (BatchSettings) xs.fromXML(fs);
}
catch (ClassCastException ex)
{
//ex.printStackTrace();
}
catch (FileNotFoundException ex)
{
ex.printStackTrace();
}
catch (XStreamException ex)
{
ex.printStackTrace();
}
finally
{
if (fs != null)
{
try
{
fs.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
return settings;
}
private Document loadDocument(String xml)
{
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder;
try
{
docBuilder = docFactory.newDocumentBuilder();
return docBuilder.parse(new ByteArrayInputStream(xml.getBytes()));
}
catch (Exception e)
{
e.printStackTrace();
}
return null;
}
private void processImage(BatchSettings settings, ImagePlus imp, ArrayList<String> xmlSettings)
{
String imageFilename = imp.getOriginalFileInfo().directory + imp.getOriginalFileInfo().fileName;
String basename = getBaseName(imp.getOriginalFileInfo().fileName);
String format = settings.resultsDirectory + File.separatorChar + basename + ".%05d";
String statusSuffix = String.format(" / %d: %s", xmlSettings.size(), imp.getOriginalFileInfo().fileName);
for (int i = 0; i < xmlSettings.size(); i++)
{
IJ.showStatus((i + 1) + statusSuffix);
// Create the configuration
FitEngineConfiguration fitConfig = null;
try
{
fitConfig = (FitEngineConfiguration) xs.fromXML(xmlSettings.get(i));
}
catch (XStreamException e)
{
// Ignore
}
if (fitConfig == null)
continue;
// No need to skip settings that do not make sense as we will catch exceptions.
// This relies on the fit engine throw exceptions for invalid settings.
// Ensure the state is restored after XStream object reconstruction
fitConfig.getFitConfiguration().initialiseState();
String prefix = String.format(format, i);
// Save the settings
String settingsFilename = saveRunSettings(prefix, imageFilename, fitConfig);
// Run the fit engine
if (settings.runPeakFit)
{
ResultsSettings resultsSettings = createResultsSettings(fitConfig, prefix);
try
{
PeakFit peakFit = new PeakFit(fitConfig, resultsSettings, settings.getCalibration());
peakFit.setSilent(true);
peakFit.run(imp, false);
IJ.log(String.format("%s : %s : Size %d : Time = %s", imageFilename, settingsFilename,
peakFit.getSize(), Utils.timeToString(peakFit.getTime())));
}
catch (Exception e)
{
// Ignore this as we assume this is from incorrect fit configuration
}
}
}
IJ.showStatus("");
}
private String getBaseName(String name)
{
int dot = name.lastIndexOf('.');
return (dot == -1) ? name : name.substring(0, dot);
}
private String saveRunSettings(String prefix, String imageFilename, FitEngineConfiguration fitConfig)
{
BatchRun batchRun = new BatchRun(imageFilename, fitConfig);
FileOutputStream fs = null;
try
{
String settingsFilename = prefix + ".xml";
fs = new FileOutputStream(settingsFilename);
xs.toXML(batchRun, fs);
return settingsFilename;
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
catch (XStreamException e)
{
e.printStackTrace();
}
finally
{
if (fs != null)
{
try
{
fs.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
return null;
}
private ResultsSettings createResultsSettings(FitEngineConfiguration fitConfig, String prefix)
{
ResultsSettings resultsSettings = new ResultsSettings();
resultsSettings.setResultsImage(ResultsImage.NONE);
resultsSettings.resultsInMemory = false;
resultsSettings.setResultsTable(ResultsTable.NONE);
resultsSettings.logProgress = false;
resultsSettings.resultsDirectory = null;
resultsSettings.showDeviations = fitConfig.getFitConfiguration().isComputeDeviations();
resultsSettings.resultsFilename = prefix + ".xls";
resultsSettings.setResultsFileFormat(ResultsFileFormat.GDSC_TEXT);
return resultsSettings;
}
/**
* Ask for parameters
*
* @return True if not cancelled
*/
private boolean showDialog()
{
GenericDialog gd = new GenericDialog(TITLE);
gd.addHelp(About.HELP_URL);
gd.addStringField("Config_filename", configFilename);
gd.addCheckbox("Create_config_file", false);
if (Utils.isShowGenericDialog())
{
configFilenameText = (TextField) gd.getStringFields().get(0);
configFilenameText.setColumns(30);
configFilenameText.addMouseListener(this);
Checkbox cb = (Checkbox) gd.getCheckboxes().get(0);
cb.addItemListener(this);
}
gd.showDialog();
if (gd.wasCanceled())
return false;
configFilename = gd.getNextString().trim();
return true;
}
/*
* (non-Javadoc)
*
* @see java.awt.event.ItemListener#itemStateChanged(java.awt.event.ItemEvent)
*/
public void itemStateChanged(ItemEvent e)
{
// When the checkbox is clicked, create a default configuration file and update the
// GenericDialog with the file location.
Checkbox cb = (Checkbox) e.getSource();
if (cb.getState())
{
cb.setState(false);
Document doc = getDefaultSettingsXmlDocument();
if (doc == null)
return;
try
{
// Look for nodes that are part of the fit configuration
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
XPathExpression expr = xpath.compile("//gdsc.smlm.engine.FitEngineConfiguration//*");
// For each node, add the name and value to the BatchParameters
BatchSettings batchSettings = new BatchSettings();
batchSettings.resultsDirectory = System.getProperty("java.io.tmpdir");
batchSettings.images.add("/path/to/image.tif");
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++)
{
Node node = nodes.item(i);
if (node.getChildNodes().getLength() == 1) // Only nodes with a single text entry
{
batchSettings.parameters.add(new ParameterSettings(node.getNodeName(), node.getTextContent()));
}
}
// Save the settings file
String[] path = Utils.decodePath(configFilenameText.getText());
OpenDialog chooser = new OpenDialog("Settings_file", path[0], path[1]);
if (chooser.getFileName() != null)
{
String newFilename = chooser.getDirectory() + chooser.getFileName();
if (!newFilename.endsWith(".xml"))
newFilename += ".xml";
FileOutputStream fs = null;
try
{
fs = new FileOutputStream(newFilename);
xs.toXML(batchSettings, fs);
}
finally
{
if (fs != null)
{
fs.close();
}
}
// Update dialog filename
configFilenameText.setText(newFilename);
}
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
private XStream createXStream()
{
XStream xs = new XStream(new DomDriver());
xs.alias("gdsc.fitting.batchSettings", BatchSettings.class);
xs.alias("parameter", ParameterSettings.class);
xs.alias("gdsc.fitting.batchRun", BatchRun.class);
xs.omitField(FitConfiguration.class, "flags");
xs.omitField(FitConfiguration.class, "signalThreshold");
//xs.omitField(FitConfiguration.class, "noise");
xs.omitField(FitConfiguration.class, "enableValidation");
return xs;
}
/*
* (non-Javadoc)
*
* @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
*/
public void mouseClicked(MouseEvent e)
{
if (e.getClickCount() > 1) // Double-click
{
if (e.getSource() == configFilenameText)
{
String[] path = Utils.decodePath(configFilenameText.getText());
OpenDialog chooser = new OpenDialog("Settings_file", path[0], path[1]);
if (chooser.getFileName() != null)
{
String newFilename = chooser.getDirectory() + chooser.getFileName();
configFilenameText.setText(newFilename);
}
}
}
}
/*
* (non-Javadoc)
*
* @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
*/
public void mousePressed(MouseEvent e)
{
}
/*
* (non-Javadoc)
*
* @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
*/
public void mouseReleased(MouseEvent e)
{
}
/*
* (non-Javadoc)
*
* @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
*/
public void mouseEntered(MouseEvent e)
{
}
/*
* (non-Javadoc)
*
* @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
*/
public void mouseExited(MouseEvent e)
{
}
}