package cz.cuni.lf1.lge.ThunderSTORM.UI;
import cz.cuni.lf1.lge.ThunderSTORM.CameraSetupPlugIn;
import cz.cuni.lf1.lge.ThunderSTORM.IModuleUI;
import cz.cuni.lf1.lge.ThunderSTORM.detectors.IDetector;
import cz.cuni.lf1.lge.ThunderSTORM.detectors.ui.IDetectorUI;
import cz.cuni.lf1.lge.ThunderSTORM.estimators.PSF.Molecule;
import cz.cuni.lf1.lge.ThunderSTORM.estimators.PSF.PSFModel;
import cz.cuni.lf1.lge.ThunderSTORM.estimators.ui.IEstimatorUI;
import cz.cuni.lf1.lge.ThunderSTORM.filters.ui.IFilterUI;
import cz.cuni.lf1.lge.ThunderSTORM.rendering.ui.IRendererUI;
import cz.cuni.lf1.lge.ThunderSTORM.thresholding.Thresholder;
import static cz.cuni.lf1.lge.ThunderSTORM.util.ImageMath.subtract;
import cz.cuni.lf1.lge.ThunderSTORM.util.PluginCommands;
import cz.cuni.lf1.lge.ThunderSTORM.util.Point;
import ij.IJ;
import ij.ImagePlus;
import ij.Prefs;
import ij.gui.Roi;
import ij.measure.Calibration;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
/**
* Dialog with settings of filters, detectors, estimators, and other parameters
* used for analysis.
*/
public class AnalysisOptionsDialog extends JDialog implements ActionListener {
private CardsPanel<IFilterUI> filtersPanel;
private CardsPanel<IDetectorUI> detectorsPanel;
private CardsPanel<IEstimatorUI> estimatorsPanel;
private CardsPanel<IRendererUI> renderersPanel;
private List<IFilterUI> allFilters;
private List<IDetectorUI> allDetectors;
private List<IEstimatorUI> allEstimators;
private List<IRendererUI> allRenderers;
private JButton cameraSetup, defaults, preview, ok, cancel;
private ImagePlus imp;
private boolean canceled;
private Semaphore semaphore; // ensures waiting for a dialog without the dialog being modal!
private int activeFilterIndex;
private int activeDetectorIndex;
private int activeEstimatorIndex;
private int activeRendererIndex;
ExecutorService previewThreadRunner = Executors.newSingleThreadExecutor();
Future<?> previewFuture = null;
/**
* Initialize and show the analysis options dialog.
*
* @param imp {@code ImagePlus} that was active when the plugin was executed
* @param title title of the frame
* @param filters vector of filter modules (they all must implement
* {@code IFilter} interface)
* @param detectors vector of detector modules (they all must implement
* {@code IDetector} interface)
* @param estimators vector of estimator modules (they all must implement
* {@code IEstimator} interface)
*/
public AnalysisOptionsDialog(final ImagePlus imp, String title,
List<IFilterUI> filters,
List<IDetectorUI> detectors,
List<IEstimatorUI> estimators,
List<IRendererUI> renderers) {
//
super(IJ.getInstance(), title);
//
this.canceled = true;
//
this.imp = imp;
//
this.cameraSetup = new JButton("Camera setup");
this.cameraSetup.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
MacroParser.runNestedWithRecording(PluginCommands.CAMERA_SETUP.getValue(), null);
}
});
//
this.allFilters = filters;
this.allDetectors = detectors;
this.allEstimators = estimators;
this.allRenderers = renderers;
//
this.activeFilterIndex = Integer.parseInt(Prefs.get("thunderstorm.filters.index", "0"));
this.activeDetectorIndex = Integer.parseInt(Prefs.get("thunderstorm.detectors.index", "0"));
this.activeEstimatorIndex = Integer.parseInt(Prefs.get("thunderstorm.estimators.index", "0"));
this.activeRendererIndex = Integer.parseInt(Prefs.get("thunderstorm.rendering.index", "0"));
//
this.filtersPanel = new CardsPanel<IFilterUI>(filters, activeFilterIndex);
this.detectorsPanel = new CardsPanel<IDetectorUI>(detectors, activeDetectorIndex);
this.estimatorsPanel = new CardsPanel<IEstimatorUI>(estimators, activeEstimatorIndex);
this.renderersPanel = new CardsPanel<IRendererUI>(renderers, activeRendererIndex);
//
this.defaults = new JButton("Defaults");
this.preview = new JButton("Preview");
this.ok = new JButton("Ok");
this.cancel = new JButton("Cancel");
//
this.semaphore = new Semaphore(0);
//
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
addComponentsToPane();
}
private void addComponentsToPane() {
JPanel pane = new JPanel();
//
pane.setLayout(new GridBagLayout());
GridBagConstraints componentConstraints = new GridBagConstraints();
componentConstraints.gridx = 0;
componentConstraints.fill = GridBagConstraints.BOTH;
componentConstraints.weightx = 1;
JPanel cameraPanel = new JPanel(new BorderLayout());
cameraPanel.add(cameraSetup);
cameraPanel.setBorder(BorderFactory.createTitledBorder("Camera"));
pane.add(cameraPanel, componentConstraints);
JPanel p = filtersPanel.getPanel("Filter:");
p.setBorder(BorderFactory.createTitledBorder("Image filtering"));
pane.add(p, componentConstraints);
p = detectorsPanel.getPanel("Method:");
p.setBorder(BorderFactory.createTitledBorder("Approximate localization of molecules"));
pane.add(p, componentConstraints);
p = estimatorsPanel.getPanel("Method:");
p.setBorder(BorderFactory.createTitledBorder("Sub-pixel localization of molecules"));
pane.add(p, componentConstraints);
p = renderersPanel.getPanel("Method:");
p.setBorder(BorderFactory.createTitledBorder("Visualisation of the results"));
pane.add(p, componentConstraints);
//
defaults.addActionListener(this);
preview.addActionListener(this);
ok.addActionListener(this);
cancel.addActionListener(this);
//
JPanel buttons = new JPanel();
buttons.add(defaults);
buttons.add(Box.createHorizontalStrut(30));
buttons.add(preview);
buttons.add(Box.createHorizontalStrut(30));
buttons.add(ok);
buttons.add(cancel);
pane.add(buttons, componentConstraints);
pane.setBorder(BorderFactory.createEmptyBorder(10, 20, 10, 20));
JScrollPane scrollPane = new JScrollPane(pane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setBorder(BorderFactory.createEmptyBorder());
add(scrollPane);
getRootPane().setDefaultButton(ok);
pack();
int maxScreenHeight = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds().height;
if(getHeight() > maxScreenHeight) {
setSize(getWidth(), maxScreenHeight);
}
}
/**
* Action handler.
*
* There are three possible actions. Canceling the analysis, confirming the
* settings of analysis, and preview the results of analysis on a single
* frame selected in active {@code ImagePlus} window.
*
* @param e event object holding the action details. It gets processed as
* follows: <ul> <li>actionCommand == "Cancel": cancel the analysis</li>
* <li>actionCommand == "Ok": confirm the settings and run the analysis</li>
* <li>actionCommand == "Preview": preview the results of analysis with the
* current selected settings on a single frame</li> </ul>
*/
@Override
public void actionPerformed(ActionEvent e) {
if(e.getActionCommand().equals("Cancel")) {
closeDialog(true);
} else if(e.getActionCommand().equals("Ok")) {
Calibration cal = new Calibration();
cal.setUnit("um");
cal.pixelWidth = cal.pixelHeight = CameraSetupPlugIn.getPixelSize() / 1000.0;
imp.setCalibration(cal);
//
activeFilterIndex = filtersPanel.getActiveComboBoxItemIndex();
activeDetectorIndex = detectorsPanel.getActiveComboBoxItemIndex();
activeEstimatorIndex = estimatorsPanel.getActiveComboBoxItemIndex();
activeRendererIndex = renderersPanel.getActiveComboBoxItemIndex();
//
try {
allFilters.get(activeFilterIndex).readParameters();
allDetectors.get(activeDetectorIndex).readParameters();
allEstimators.get(activeEstimatorIndex).readParameters();
allRenderers.get(activeRendererIndex).readParameters();
saveSelectedModuleIndexesToPrefs(activeFilterIndex, activeDetectorIndex, activeEstimatorIndex, activeRendererIndex);
} catch(Exception ex) {
IJ.error("Error parsing parameters: " + ex.toString());
return;
}
//
closeDialog(false);
} else if(e.getActionCommand().equals("Preview")) {
activeFilterIndex = filtersPanel.getActiveComboBoxItemIndex();
activeDetectorIndex = detectorsPanel.getActiveComboBoxItemIndex();
activeEstimatorIndex = estimatorsPanel.getActiveComboBoxItemIndex();
activeRendererIndex = renderersPanel.getActiveComboBoxItemIndex();
//
try {
allFilters.get(activeFilterIndex).readParameters();
allDetectors.get(activeDetectorIndex).readParameters();
allEstimators.get(activeEstimatorIndex).readParameters();
allRenderers.get(activeRendererIndex).readParameters();
allFilters.get(activeFilterIndex).resetThreadLocal();
allDetectors.get(activeDetectorIndex).resetThreadLocal();
allEstimators.get(activeEstimatorIndex).resetThreadLocal();
allRenderers.get(activeRendererIndex).resetThreadLocal();
saveSelectedModuleIndexesToPrefs(activeFilterIndex, activeDetectorIndex, activeEstimatorIndex, activeRendererIndex);
} catch(Exception ex) {
IJ.error("Error parsing parameters: " + ex.toString());
return;
}
//
try {
Thresholder.loadFilters(allFilters);
Thresholder.setActiveFilter(activeFilterIndex); // !! must be called before any threshold is evaluated !!
Thresholder.parseThreshold(allDetectors.get(activeDetectorIndex).getThreadLocalImplementation().getThresholdFormula());
} catch(Exception ex) {
IJ.error("Error parsing threshold formula! " + ex.toString());
}
//
if(previewFuture != null) {
previewFuture.cancel(true);
}
final Roi roi = imp.getRoi();
previewFuture = previewThreadRunner.submit(new Runnable() {
@Override
public void run() {
try {
IJ.showStatus("Creating preview image.");
ImageProcessor processor = imp.getProcessor();
if(roi != null) {
processor.setRoi(roi.getBounds());
processor = processor.crop();
} else {
processor = processor.duplicate();
}
FloatProcessor fp = subtract((FloatProcessor) processor.crop().convertToFloat(), (float) CameraSetupPlugIn.getOffset());
if(roi != null) {
fp.setMask(roi.getMask());
}
Thresholder.setCurrentImage(fp);
FloatProcessor filtered = allFilters.get(activeFilterIndex).getThreadLocalImplementation().filterImage(fp);
new ImagePlus("Filtered frame " + Integer.toString(imp.getSlice()), filtered).show();
GUI.checkIJEscapePressed();
IDetector detector = allDetectors.get(activeDetectorIndex).getThreadLocalImplementation();
List<Point> detections = Point.applyRoiMask(imp.getRoi(), detector.detectMoleculeCandidates(filtered));
ij.measure.ResultsTable tbl = ij.measure.ResultsTable.getResultsTable();
tbl.reset();
tbl.incrementCounter();
tbl.addValue("Threshold value for frame " + Integer.toString(imp.getSlice()), detector.getThresholdValue());
tbl.show("Results");
GUI.checkIJEscapePressed();
List<Molecule> results = allEstimators.get(activeEstimatorIndex).getThreadLocalImplementation().estimateParameters(fp, detections);
GUI.checkIJEscapePressed();
//
ImagePlus impPreview = new ImagePlus("Detections in frame " + Integer.toString(imp.getSlice()), processor);
RenderingOverlay.showPointsInImage(impPreview,
Molecule.extractParamToArray(results, PSFModel.Params.LABEL_X),
Molecule.extractParamToArray(results, PSFModel.Params.LABEL_Y),
Color.red, RenderingOverlay.MARKER_CROSS);
impPreview.show();
} catch(StoppedByUserException ex) {
IJ.resetEscape();
IJ.showStatus("Preview interrupted.");
} catch(Exception ex) {
IJ.handleException(ex);
}
}
});
} else if(e.getActionCommand().equals("Defaults")) {
//noinspection unchecked
resetModuleUIs(allDetectors, allEstimators, allFilters, allRenderers);
resetCardsPanels(detectorsPanel, estimatorsPanel, filtersPanel, renderersPanel);
} else {
throw new UnsupportedOperationException("Command '" + e.getActionCommand() + "' is not supported!");
}
}
/**
* Override the default {@code JDialog.dispose} method to release the
* {@code semaphore} (see {
*
* @wasCanceled}).
*/
@Override
public void dispose() {
super.dispose();
semaphore.release();
if(previewThreadRunner != null) {
previewThreadRunner.shutdownNow();
}
}
/**
* Close (dispose) the dialog.
*
* @param cancel is the dialog closing because the operation has been
* canceled?
*/
public void closeDialog(boolean cancel) {
canceled = cancel;
dispose();
}
/**
* Query if the dialog was closed by canceling (cancel button or red cross
* window button) or by clicking on OK.
*
* <strong>This is a blocking call!</strong> Meaning that when creating a
* non-modal dialog it is created and runs in its own thread and does not
* block the creator thread. Call this method, however, calls
* {@code semaphore.acquire}, which is a blocking call and waits until the
* semaphore is released (if it wasn't already) which is done after closing
* the dialog. Clearly, if this wasn't a blocking call, there wouldn't be a
* way to know how was the dialog closed, because it wouldn't need to be
* closed at the time of calling this method.
*
* @return {@code true} if the dialog was canceled, {@code false} otherwise
*/
public boolean wasCanceled() {
try {
semaphore.acquire();
} catch(InterruptedException ex) {
IJ.handleException(ex);
}
return canceled;
}
/**
* Return a filter selected from the combo box.
*
* @return selected filter
*/
public IFilterUI getFilter() {
return allFilters.get(activeFilterIndex);
}
/**
* Return a detector selected from the combo box.
*
* @return selected detector
*/
public IDetectorUI getDetector() {
return allDetectors.get(activeDetectorIndex);
}
/**
* Return an estimator selected from the combo box.
*
* @return selected estimator
*/
public IEstimatorUI getEstimator() {
return allEstimators.get(activeEstimatorIndex);
}
public IRendererUI getRenderer() {
return allRenderers.get(activeRendererIndex);
}
public int getFilterIndex() {
return activeFilterIndex;
}
public int getDetectorIndex() {
return activeDetectorIndex;
}
public int getEstimatorIndex() {
return activeEstimatorIndex;
}
public int getRendererIndex() {
return activeRendererIndex;
}
public static void resetModuleUIs(List<? extends IModuleUI>... lists) {
for (List<? extends IModuleUI> list : lists) {
for (IModuleUI module : list) {
module.resetToDefaults();
}
}
}
static void resetCardsPanels(CardsPanel<?>... panels) {
for (CardsPanel<?> panel : panels) {
panel.setSelectedItemIndex(0);
}
}
static void saveSelectedModuleIndexesToPrefs(int filterIndex, int detectorIndex, int estimatorIndex, int rendererIndex) {
if(filterIndex >= 0) {
Prefs.set("thunderstorm.filters.index", filterIndex);
}
if(detectorIndex >= 0) {
Prefs.set("thunderstorm.detectors.index", detectorIndex);
}
if(estimatorIndex >= 0) {
Prefs.set("thunderstorm.estimators.index", estimatorIndex);
}
if(rendererIndex >= 0) {
Prefs.set("thunderstorm.rendering.index", rendererIndex);
}
}
}