package gdsc.smlm.ij.results; /*----------------------------------------------------------------------------- * 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.function.gaussian.Gaussian2DFunction; import gdsc.smlm.ij.utils.CoordinateProvider; import gdsc.smlm.ij.utils.ImageROIPainter; import gdsc.smlm.results.Calibration; import gdsc.smlm.results.PeakResult; import ij.WindowManager; import ij.text.TextPanel; import ij.text.TextWindow; import java.awt.Frame; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; /** * Saves the fit results to an ImageJ results table. * <p> * The table supports mouse click events to draw the selected coordinates on the original source image using the * ImageROIPainter. */ public class IJTablePeakResults extends IJAbstractPeakResults implements CoordinateProvider { // Store the ROI painters that have been attached to TextPanels so they can be updated // with a new image source private static HashMap<TextPanel, ImageROIPainter> map = new HashMap<TextPanel, ImageROIPainter>(); private boolean showDeviations = true; private boolean showEndFrame = false; private boolean clearAtStart = false; private boolean showCalibratedValues = false; private boolean hideSourceText = false; private String peakIdColumnName = "Peak"; private String source = null; private String sourceText = null; private String tableTitle = "Fit Results"; private TextWindow resultsWindow; private TextPanel tp; private double gain = 1; private double nmPerPixel = 1; private boolean addCounter = false; protected boolean tableActive = false; private int nextRepaintSize = 0; private double repaintInterval = 0.1; private int indexT = -1, indexX = -1, indexY = -1; private int size = 0; public IJTablePeakResults(boolean showDeviations) { this.showDeviations = showDeviations; } public IJTablePeakResults(boolean showDeviations, String source) { this.showDeviations = showDeviations; this.source = source; } public IJTablePeakResults(boolean showDeviations, String source, boolean clearAtStart) { this.showDeviations = showDeviations; this.source = source; this.clearAtStart = clearAtStart; } /* * (non-Javadoc) * * @see gdsc.utils.fitting.PeakResults#begin() */ public void begin() { tableActive = false; createSourceText(); createResultsWindow(); if (clearAtStart) { tp.clear(); } if (showCalibratedValues) { Calibration cal = getCalibration(); if (cal != null) { gain = cal.getGain(); nmPerPixel = cal.getNmPerPixel(); } else { gain = 1; nmPerPixel = 1; } } size = 0; // Let some results appear before drawing. // ImageJ will auto-layout columns if it has less than 10 rows nextRepaintSize = 9; tableActive = true; } /** * Create the result window (if it is not available) */ private void createResultsWindow() { String header = createResultsHeader(); ImageROIPainter roiPainter = null; for (Frame f : WindowManager.getNonImageWindows()) { if (f != null && tableTitle.equals(f.getTitle()) && f instanceof TextWindow) { resultsWindow = (TextWindow) f; // Check if the existing table matches the desired header String currentHeader = resultsWindow.getTextPanel().getColumnHeadings(); if (!currentHeader.startsWith(header)) { resultsWindow = null; continue; } roiPainter = map.get(resultsWindow.getTextPanel()); break; } } if (resultsWindow == null || !resultsWindow.isShowing()) { resultsWindow = new TextWindow(tableTitle, header, "", 800, 300); roiPainter = new ImageROIPainter(resultsWindow.getTextPanel(), "", this); // The ROI painter adds itself to the TextPanel as a mouse listener. However // the TextPanel addMouseListener() adds to the private TextCanvas object so it // cannot be retrieved. Store the painter in a global lookup table. map.put(resultsWindow.getTextPanel(), roiPainter); } tp = resultsWindow.getTextPanel(); if (roiPainter != null && getSource() != null) { roiPainter.setTitle(getSource().getOriginal().getName()); // Update the coordinate provider (avoids memory leaks with old objects lying around) roiPainter.setCoordProvider(this); // Get the headings for extracting the coordinates String[] headings = tp.getColumnHeadings().split("\t"); for (int i = 0; i < headings.length; i++) { if (headings[i].equals(peakIdColumnName)) { indexT = i; continue; } if (headings[i].equals("X")) { indexX = i; continue; } if (headings[i].equals("Y")) { indexY = i; continue; } } } } private String createResultsHeader() { StringBuilder sb = new StringBuilder(); if (addCounter) sb.append("#\t"); if (sourceText != null) sb.append("Source\t"); sb.append(peakIdColumnName); if (showEndFrame) sb.append("\tEnd ").append(peakIdColumnName); sb.append("\torigX"); sb.append("\torigY"); sb.append("\torigValue"); sb.append("\tError"); sb.append("\tNoise"); sb.append("\tSNR"); sb.append("\tBackground"); addDeviation(sb); sb.append("\tSignal"); addDeviation(sb); sb.append("\tAngle"); addDeviation(sb); sb.append("\tX"); addDeviation(sb); sb.append("\tY"); addDeviation(sb); sb.append("\tX SD"); addDeviation(sb); sb.append("\tY SD"); addDeviation(sb); if (this.calibration != null) { sb.append("\tPrecision (nm)"); } return sb.toString(); } private void createSourceText() { if (hideSourceText) { sourceText = null; return; } StringBuilder sb = new StringBuilder(); if (source != null) sb.append(source); else if (getSource() != null) sb.append(getSource().getName()); if (bounds != null) { if (sb.length() > 0) sb.append(": "); sb.append(getBoundsString()); } if (sb.length() > 0) { sb.append("\t"); sourceText = sb.toString(); } } private void addDeviation(StringBuilder sb) { if (showDeviations) sb.append("\t+/-"); } /* * (non-Javadoc) * * @see gdsc.smlm.results.AbstractPeakResults#add(int, int, int, float, double, float, float[], float[]) */ public void add(int peak, int origX, int origY, float origValue, double error, float noise, float[] params, float[] paramsDev) { addPeak(peak, peak, origX, origY, origValue, error, noise, params, paramsDev); } private void addPeak(int peak, int endFrame, int origX, int origY, float origValue, double error, float noise, float[] params, float[] paramsDev) { if (!tableActive) return; float precision = 0; if (this.calibration != null) { final double s = (params[Gaussian2DFunction.X_SD] + params[Gaussian2DFunction.Y_SD]) * 0.5 * calibration.getNmPerPixel(); precision = (float) PeakResult.getPrecision(calibration.getNmPerPixel(), s, params[Gaussian2DFunction.SIGNAL] / calibration.getGain(), noise / calibration.getGain(), calibration.isEmCCD()); } final float snr = (noise > 0) ? params[Gaussian2DFunction.SIGNAL] / noise : 0; if (showCalibratedValues) { // Do not calibrate the original values //origX *= nmPerPixel; //origY *= nmPerPixel; //origValue /= gain; noise /= gain; params = Arrays.copyOf(params, params.length); params[Gaussian2DFunction.SIGNAL] /= gain; params[Gaussian2DFunction.BACKGROUND] = (float) ((params[Gaussian2DFunction.BACKGROUND] - calibration.getBias()) / gain); params[Gaussian2DFunction.X_POSITION] *= nmPerPixel; params[Gaussian2DFunction.X_SD] *= nmPerPixel; params[Gaussian2DFunction.Y_POSITION] *= nmPerPixel; params[Gaussian2DFunction.Y_SD] *= nmPerPixel; if (paramsDev != null) { paramsDev = Arrays.copyOf(paramsDev, paramsDev.length); paramsDev[Gaussian2DFunction.SIGNAL] /= gain; paramsDev[Gaussian2DFunction.BACKGROUND] /= gain; paramsDev[Gaussian2DFunction.X_POSITION] *= nmPerPixel; paramsDev[Gaussian2DFunction.X_SD] *= nmPerPixel; paramsDev[Gaussian2DFunction.Y_POSITION] *= nmPerPixel; paramsDev[Gaussian2DFunction.Y_SD] *= nmPerPixel; } } if (showDeviations) { if (paramsDev == null) paramsDev = new float[7]; addResult(peak, endFrame, origX, origY, origValue, error, noise, snr, params[0], paramsDev[0], params[1], paramsDev[1], params[2], paramsDev[2], params[3], paramsDev[3], params[4], paramsDev[4], params[5], paramsDev[5], params[6], paramsDev[6], precision); } else { addResult(peak, endFrame, origX, origY, origValue, error, noise, snr, params[0], params[1], params[2], params[3], params[4], params[5], params[6], precision); } } private void addResult(int peak, int endPeak, float origX, float origY, float origValue, double error, float... args) { StringBuilder sb = new StringBuilder(); if (addCounter) sb.append(size + 1).append("\t"); if (sourceText != null) sb.append(sourceText); // Do not calibrate the original values //if (showCalibratedValues) // sb.append(peak).append(String.format("\t%g", origX)).append(String.format("\t%g", origY)); //else sb.append(peak); if (showEndFrame) sb.append("\t").append(endPeak); sb.append(String.format("\t%.0f", origX)).append(String.format("\t%.0f", origY)); sb.append(String.format("\t%g", origValue)); sb.append(String.format("\t%g", error)); for (float f : args) sb.append(String.format("\t%g", f)); append(sb.toString()); } private void append(String result) { // Support for periodic refresh synchronized (tp) { addResult(result); } updateTable(); } private void addResult(String result) { size++; tp.appendWithoutUpdate(result); } private void updateTable() { if (size < nextRepaintSize) return; if (!resultsWindow.isShowing()) { //System.out.println("Table has been closed"); tableActive = false; return; } drawTable(); } private void drawTable() { synchronized (tp) { //System.out.printf("Refreshing table: size = %d\n", size); nextRepaintSize = (int) (size + size * repaintInterval); tp.updateDisplay(); } } public void addAll(Collection<PeakResult> results) { if (!tableActive) return; int n = 0; for (PeakResult result : results) { addPeak(result.getFrame(), result.getEndFrame(), result.origX, result.origY, result.origValue, result.error, result.noise, result.params, result.paramsStdDev); if (n++ > 31) { if (!tableActive) return; n = 0; } } } /* * (non-Javadoc) * * @see gdsc.utils.fitting.PeakResults#size() */ public int size() { return size; } /* * (non-Javadoc) * * @see gdsc.utils.fitting.PeakResults#end() */ public void end() { tableActive = false; drawTable(); } /** * @return the name of the peak column */ public String getPeakIdColumnName() { return peakIdColumnName; } /** * @param peakIdColumnName * the name of the peak column */ public void setPeakIdColumnName(String peakIdColumnName) { this.peakIdColumnName = peakIdColumnName; } /* * (non-Javadoc) * * @see gdsc.utils.fitting.results.PeakResults#isActive() */ public boolean isActive() { return tableActive; } /** * @return the table title */ public String getTableTitle() { return tableTitle; } /** * Use to set the title of the table. If an existing table exists with the same title then it will be appended, * otherwise a new table is created. * * @param tableTitle * the table title */ public void setTableTitle(String tableTitle) { if (tableTitle != null && tableTitle.length() > 0) this.tableTitle = tableTitle; } /** * @return True if the deviations of the parameters should be shown */ public boolean isShowDeviations() { return showDeviations; } /** * @param showDeviations * True if the deviations of the parameters should be shown */ public void setShowDeviations(boolean showDeviations) { this.showDeviations = showDeviations; } /** * @return True if the table should be cleared in {@link #begin()} */ public boolean isClearAtStart() { return clearAtStart; } /** * @param clearAtStart * True if the table should be cleared in {@link #begin()} */ public void setClearAtStart(boolean clearAtStart) { this.clearAtStart = clearAtStart; } /** * @return True if the calibration shoudl be used to adjust the table values */ public boolean isShowCalibratedValues() { return showCalibratedValues; } /** * @param showCalibratedValues * True if the calibration shoudl be used to adjust the table values */ public void setShowCalibratedValues(boolean showCalibratedValues) { this.showCalibratedValues = showCalibratedValues; } /** * @return the addCounter */ public boolean isAddCounter() { return addCounter; } /** * @param addCounter * the addCounter to set */ public void setAddCounter(boolean addCounter) { this.addCounter = addCounter; } /** * Checks if the source text will be added to each entry. * * @return true, if hiding the source text */ public boolean isHideSourceText() { return hideSourceText; } /** * Sets the hide source text flag. * * @param hideSourceText * the new hide source text flag */ public void setHideSourceText(boolean hideSourceText) { this.hideSourceText = hideSourceText; } /** * @return the resultsWindow */ public TextWindow getResultsWindow() { return resultsWindow; } /** * @param line * @return */ public double[] getCoordinates(String line) { // Extract the startT and x,y coordinates from the PeakResult line String[] fields = line.split("\t"); try { int startT = Integer.valueOf(fields[indexT]); double x = Double.valueOf(fields[indexX]); double y = Double.valueOf(fields[indexY]); if (showCalibratedValues) { x /= nmPerPixel; y /= nmPerPixel; } return new double[] { startT, x, y }; } catch (ArrayIndexOutOfBoundsException e) { // Will happen if any index is still at the default of -1 or if there are not enough fields } catch (NumberFormatException e) { // In case any field is not a number } return null; } /** * @return If true show the results end frame in the table */ public boolean isShowEndFrame() { return showEndFrame; } /** * @param showEndFrame * If true show the results end frame in the table */ public void setShowEndFrame(boolean showEndFrame) { this.showEndFrame = showEndFrame; } /** * Image will be repainted when a fraction of new results have been added. * * @param repaintInterval * the repaintInterval to set (range 0.001-1) */ public void setRepaintInterval(double repaintInterval) { if (repaintInterval < 0.001) repaintInterval = 0.001; if (repaintInterval > 1) repaintInterval = 1; this.repaintInterval = repaintInterval; } /** * @return the repaintInterval */ public double getRepaintInterval() { return repaintInterval; } }