ANA_.java

package com.github.celldynamics.quimp.plugin.ana;

import java.awt.AWTEvent;
import java.awt.Checkbox;
import java.awt.Choice;
import java.awt.Color;
import java.awt.Polygon;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.celldynamics.quimp.CellStats;
import com.github.celldynamics.quimp.FrameStatistics;
import com.github.celldynamics.quimp.Outline;
import com.github.celldynamics.quimp.OutlineHandler;
import com.github.celldynamics.quimp.QParams;
import com.github.celldynamics.quimp.QParamsQconf;
import com.github.celldynamics.quimp.QuimP;
import com.github.celldynamics.quimp.QuimpException;
import com.github.celldynamics.quimp.QuimpException.MessageSinkTypes;
import com.github.celldynamics.quimp.Vert;
import com.github.celldynamics.quimp.filesystem.ANAParamCollection;
import com.github.celldynamics.quimp.filesystem.DataContainer;
import com.github.celldynamics.quimp.filesystem.OutlinesCollection;
import com.github.celldynamics.quimp.filesystem.converter.FormatConverter;
import com.github.celldynamics.quimp.geom.ExtendedVector2d;
import com.github.celldynamics.quimp.plugin.AbstractPluginQconf;
import com.github.celldynamics.quimp.plugin.QuimpPluginException;
import com.github.celldynamics.quimp.plugin.ecmm.ECMM_Mapping;
import com.github.celldynamics.quimp.plugin.ecmm.ECMp;
import com.github.celldynamics.quimp.plugin.ecmm.ODEsolver;
import com.github.celldynamics.quimp.utils.QuimPArrayUtils;
import com.github.celldynamics.quimp.utils.QuimpToolsCollection;

import ij.IJ;
import ij.ImagePlus;
import ij.Prefs;
import ij.WindowManager;
import ij.gui.DialogListener;
import ij.gui.GenericDialog;
import ij.gui.Overlay;
import ij.gui.PointRoi;
import ij.gui.PolygonRoi;
import ij.gui.Roi;
import ij.gui.YesNoCancelDialog;
import ij.measure.Measurements;
import ij.measure.ResultsTable;
import ij.plugin.Converter;
import ij.plugin.filter.Analyzer;
import ij.process.ImageProcessor;
import ij.process.ImageStatistics;

/**
 * Main ANA class implementing IJ PlugInFilter.
 * 
 * @author tyson
 */
public class ANA_ extends AbstractPluginQconf implements DialogListener {

  private static String thisPluginName = "ANA";

  /**
   * The Constant LOGGER.
   */
  static final Logger LOGGER = LoggerFactory.getLogger(ANA_.class.getName());

  private OutlineHandler oh; // set by runFrom*
  private OutlineHandler outputH;
  private OutlineHandler ecmH;
  private OutlinesCollection outputOutlineHandlers; // output for new data file
  private Outline frameOneClone;
  private ECMM_Mapping ecmMapping;
  private Overlay overlay;
  // outlines can be plotted separately. They are generated by Ana() and stored here
  private ArrayList<Roi> storedOuterROI; // outer outline for each frame for all cells
  private ArrayList<Roi> storedInnerROI; // inner outline for each frame for all cells

  private ImagePlus setupImage; // image fluoro
  private ImageProcessor orgIpr; // passed by setup

  /**
   * ANA extends statistics generated by BOA by fluorescence related data.
   * 
   * <p>This is object that holds stats read from stQP file.
   */
  private FrameStatistics[] fluoStats;
  private ANAp anap;
  private static final int m =
          Measurements.AREA + Measurements.INTEGRATED_DENSITY + Measurements.MEAN;

  /**
   * Default constructor called always.
   */
  public ANA_() {
    super(new AnaOptions(), thisPluginName);
    storedOuterROI = new ArrayList<>();
    storedInnerROI = new ArrayList<>();
    anap = new ANAp();
    ECMp.plot = false;
    ecmMapping = new ECMM_Mapping(1);
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginBase#run(java.lang.String)
   */
  @Override
  public void run(String arg) {
    // overcome problem, IJ UI somehow break this so show before or IJ.run below
    // publishMacroString("ANA");
    setupImage = WindowManager.getCurrentImage();
    if (setupImage == null) {
      IJ.error("Image required to take fluoresence measurments.");
      return;
    }
    if (setupImage.getOriginalFileInfo() == null
            || setupImage.getOriginalFileInfo().directory.matches("")) {
      IJ.log("Error: Fluorescence file needs to be saved to disk");
      IJ.error("Please save your fluorescence image to file.");
      return;
    }
    Prefs.interpolateScaledImages = false; // switch off interpolation of zoomed images
    // IJ.run("Appearance...", " menu=0"); // switch off interpolation of zoomed images
    overlay = new Overlay();
    setupImage.setOverlay(overlay);
    orgIpr = setupImage.getProcessor();
    super.run(arg);
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginQconf#executer()
   */
  @Override
  protected void executer() throws QuimpException {
    super.executer(); // will run runFrom*
    // post-processing
    if (qconfLoader.getQp() == null) {
      return; // cancelled
    }
    // and then do the rest
    AnaOptions opts = (AnaOptions) options;
    // post-plotting
    overlay = new Overlay();
    setupImage.setOverlay(overlay);
    for (int f = 1; f < setupImage.getStackSize(); f++) {
      setupImage.setSlice(f);
      for (OutlineHandler ohTmp : outputOutlineHandlers.oHs) {
        if (f >= ohTmp.getStartFrame() && f <= ohTmp.getEndFrame()) {
          Outline o = ohTmp.getStoredOutline(f);
          if (o == null) { // should not happen
            continue;
          }
          drawSamplePointsFloat(o, f);
          setupImage.draw();
        }
      }
    }
    // plotting outlines on separate image
    if (opts.plotOutlines) {
      ImagePlus orgIplclone = setupImage.duplicate();
      orgIplclone.show();
      new Converter().run("RGB Color");
      Overlay overlay = new Overlay();
      orgIplclone.setOverlay(overlay);
      for (Roi r : storedOuterROI) {
        overlay.add(r);
      }
      for (Roi r : storedInnerROI) {
        overlay.add(r);
      }
      orgIplclone.draw();
    }

    // edd results to IJtable named Results - to allow Summarise
    if (opts.fluoResultTable || opts.fluoResultTableAppend) {
      if (qconfLoader.isFileLoaded() == QParams.NEW_QUIMP) {
        ResultsTable rt;
        if (opts.fluoResultTableAppend) { // get current table
          rt = Analyzer.getResultsTable();
        } else { // or create new
          rt = new ResultsTable();
          Analyzer.setResultsTable(rt);
        }
        // iterate over cells - all cells for this experiment are cumulated in one table
        for (CellStats cs : qconfLoader.getStats().getStatCollection()) {
          cs.addFluosToResultTable(rt, opts.channel);
        }
        rt.show("Results");
      } else {
        LOGGER.warn(
                "Results can be shown in IJ table only if ANA is started with QCONF file format");
      }
    }
    ecmMapping = null;
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginQconf#validate()
   */
  @Override
  protected void validate() throws QuimpException {
    qconfLoader.getBOA();
    qconfLoader.getEcmm(); // verify whether ecmm has been run (throws if not)
    qconfLoader.getStats(); // verify whether file contains stats
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.IQuimpPlugin#about()
   */
  @Override
  public String about() {
    return "ANA plugin.\n" + "Authors: Piotr Baniukiewicz\n"
            + "mail: p.baniukiewicz@warwick.ac.uk\n" + "Richard Tyson";
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginFilterQconf#runFromQconf()
   */
  @Override
  protected void runFromQconf() throws QuimpException {
    AnaOptions opts = (AnaOptions) options;
    LOGGER.debug("Processing from new file format");
    if (apiCall == false && errorSink == MessageSinkTypes.GUI && qconfLoader.isANAPresent()) {
      YesNoCancelDialog ync;
      ync = new YesNoCancelDialog(IJ.getInstance(), "Overwrite",
              "You are about to override previous ANA results. Is it ok? "
                      + "If you are adding new channel to the existing analysis answer Yes.");
      if (!ync.yesPressed()) { // if no or cancel
        throw new QuimpPluginException("Cancelled - no changes made to the input file",
                MessageSinkTypes.MESSAGE, true);
      }
    }

    QParamsQconf qp = (QParamsQconf) qconfLoader.getQp();
    ANAParamCollection anaStates;
    OutlinesCollection ecmmState = qp.getLoadedDataContainer().ECMMState;
    outputOutlineHandlers = new OutlinesCollection(ecmmState.oHs.size());
    if (qp.getLoadedDataContainer().getANAState() == null) {
      // create ANA slots for all outlines
      anaStates = new ANAParamCollection(ecmmState.oHs.size()); // store ANA options for every cell
    } else {
      anaStates = qp.getLoadedDataContainer().getANAState(); // update old
    }
    try {
      // sanity check for stats - they can be empty if QCONF results from conversion from paQP
      // without`
      // stQP present
      if (qconfLoader.getStats().sHs.isEmpty()) {
        throw new QuimpPluginException("Stats not found in QCONF file.");
      }
      for (int i = 0; i < ecmmState.oHs.size(); i++) { // go over all outlines
        // For compatibility, all methods have the same syntax (assumes that there is only one
        // handler)
        qp.setActiveHandler(i); // set current handler number.
        oh = ecmmState.oHs.get(i); // restore handler from ecmm
        anap = anaStates.aS.get(i); // get i-th ana parameters
        anap.setup(qconfLoader.getQp());

        // get stats stored in QCONF, they are extended by ANA (ChannelStat field)
        fluoStats = qconfLoader.getStats().sHs.get(i).framestat.toArray(new FrameStatistics[0]);

        investigateChannels(oh.indexGetOutline(0));// find first empty channel, change anap
        if (anap.noData && oh.getSize() == 1) {
          // only one frame, so no ECMM. set outline res to 2
          System.out.println("Only one frame. set marker res to 2");
          oh.indexGetOutline(0).setResolution(anap.oneFrameRes); // should be 2!!!
        }
        setImageScale();
        setupImage.setSlice(qconfLoader.getQp().getStartFrame());
        // openadialog only if called from IJ, apiCall==false for all IJ calls, so check also sink
        // to find if run from macros
        if (apiCall == false && errorSink != MessageSinkTypes.IJERROR && !anaDialog()) {
          IJ.log("ANA cancelled");
          return;
        } else { // macro, do part of anaDialog
          frameOneClone = (Outline) oh.indexGetOutline(0).clone(); // FIXME Change to copy construc
          anap.setCortextWidthScale(opts.userScale); // set scale from macro instead from UI
          if (opts.clearFlu && !anap.cleared) {
            resetFluo();
          }
        }
        anap.fluTiffs[opts.channel] = new File(setupImage.getOriginalFileInfo().directory,
                setupImage.getOriginalFileInfo().fileName);
        outputH = new OutlineHandler(oh); // copy input to output (ana will add fields to it)
        runPlugin(); // fills outputH and ChannelStat in FrameStatistics
        // save fluoro always statFile if old format selected
        if (QuimP.newFileFormat.get() == false || QuimP.newFileFormat.get() == true) {
          FrameStatistics.write(fluoStats, anap.statFile, anap.scale, anap.frameInterval);
        }
        CellStats statH = qconfLoader.getStats().sHs.get(i); // store fluoro in QCONF
        statH.framestat = new ArrayList<FrameStatistics>(Arrays.asList(fluoStats)); // store stats
        outputOutlineHandlers.oHs.add(i, new OutlineHandler(outputH)); // store actual result cont
      }

      DataContainer dc = qp.getLoadedDataContainer();
      dc.ECMMState = outputOutlineHandlers; // assign ECMM container to global output
      dc.ANAState = anaStates;
      qp.writeParams(); // save global container
    } catch (IOException e) {
      throw new QuimpPluginException(e);
    }
    // generate additional OLD files (stQP is generated in loop already), disabled #263, enabled 228
    if (QuimP.newFileFormat.get() == false) {
      FormatConverter foramtConv = new FormatConverter(qconfLoader);
      foramtConv.doConversion();
    }
    IJ.log("The new data file " + qconfLoader.getQp().getParamFile().toString()
            + " has been updated by results of ECMM analysis.");
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginFilterQconf#runFromPaqp()
   */
  @Override
  protected void runFromPaqp() throws QuimpException {
    AnaOptions opts = (AnaOptions) options;
    outputOutlineHandlers = new OutlinesCollection(1);
    oh = new OutlineHandler(qconfLoader.getQp());
    try {
      anap.setup(qconfLoader.getQp());
      fluoStats = FrameStatistics.read(anap.statFile);
      investigateChannels(oh.indexGetOutline(0));// find first empty channel

      if (anap.noData && oh.getSize() == 1) {
        // only one frame, so no ECMM. set outline res to 2
        System.out.println("Only one frame. set marker res to 2");
        oh.indexGetOutline(0).setResolution(anap.oneFrameRes); // should be 2!!!
      }

      setImageScale();
      setupImage.setSlice(qconfLoader.getQp().getStartFrame());
      if (!oh.readSuccess) {
        throw new QuimpException("Could not read OutlineHandler");
      }
      // openadialog only if called from IJ
      if (apiCall == false && errorSink != MessageSinkTypes.IJERROR && !anaDialog()) {
        IJ.log("ANA cancelled");
        return;
      } else { // macro, do part of anaDialog
        frameOneClone = (Outline) oh.indexGetOutline(0).clone(); // FIXME Change to copy construc
        anap.setCortextWidthScale(opts.userScale);
        if (opts.clearFlu && !anap.cleared) {
          resetFluo();
        }
      }
      System.out.println("CHannel: " + (opts.channel + 1));
      // qp.cortexWidth = ANAp.cortexWidthScale;
      anap.fluTiffs[opts.channel] = new File(setupImage.getOriginalFileInfo().directory,
              setupImage.getOriginalFileInfo().fileName);

      outputH = new OutlineHandler(oh.getStartFrame(), oh.getEndFrame());
      runPlugin(); // fills outputH and ChannelStat in FrameStatistics

      anap.inFile.delete();
      anap.statFile.delete();
      outputH.writeOutlines(anap.outFile, qconfLoader.getQp().isEcmmHasRun());
      FrameStatistics.write(fluoStats, anap.statFile, anap.scale, anap.frameInterval);

      // ----Write temp files-------
      // File tempFile = new File(ANAp.outFile.getAbsolutePath() +
      // ".tempANA.txt");
      // outputH.writeOutlines(tempFile);
      // File tempStats = new File(ANAp.statFile.getAbsolutePath() +
      // ".tempStats.csv");
      // FluoStats.write(fluoStats, tempStats);
      // IJ.log("ECMM:137, saving to a temp file instead");
      // --------------------------

      qconfLoader.getQp().cortexWidth = anap.getCortexWidthScale();
      qconfLoader.getQp().fluTiffs = anap.fluTiffs;
      qconfLoader.getQp().writeParams();
    } catch (IOException e) {
      throw new QuimpPluginException(e);
    }
    outputOutlineHandlers.oHs.add(0, new OutlineHandler(outputH)); // for plotting purposes

  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginBase#showUi(boolean)
   */
  @Override
  public void showUi(boolean val) throws Exception {
    // load on GUI show as well
    executer();
    if (qconfLoader != null && qconfLoader.getQp() != null) {
      options.paramFile = qconfLoader.getQp().getParamFile().getAbsolutePath();
    }
  }

  /**
   * Show dialog.
   * 
   * @return true if OK pressed
   */
  private boolean anaDialog() {
    AnaOptions opts = (AnaOptions) options;
    GenericDialog pd = new GenericDialog("ANA Dialog", IJ.getInstance());
    // initialise scale UI from QCONF
    pd.addNumericField("Cortex width (\u00B5m)", anap.getCortexWidthScale(), 2);

    String[] channelC = { "1", "2", "3" };
    pd.addChoice("Save in channel", channelC, channelC[opts.channel]);
    pd.addCheckbox("Normalise to interior", opts.normalise);
    pd.addCheckbox("Sample at Ch" + (anap.useLocFromCh + 1) + " locations", opts.sampleAtSame);
    pd.addCheckbox("Clear stored measurements", false);
    pd.addCheckbox("New image with outlines? ", opts.plotOutlines);
    pd.addCheckbox("Copy results to IJ Table?", opts.fluoResultTable);
    pd.addCheckbox("Append results to IJ Table?", opts.fluoResultTableAppend);
    pd.addDialogListener(this);

    frameOneClone = (Outline) oh.indexGetOutline(0).clone(); // FIXME Change to copy constructor
    drawOutlineAsOverlay(frameOneClone, Color.RED);
    shrink(frameOneClone);
    this.markFrozenNodesNormal(frameOneClone);
    setupImage.draw();
    drawOutlineAsOverlay(frameOneClone, Color.RED);
    pd.showDialog();

    return pd.wasOKed();

  }

  /*
   * (non-Javadoc)
   * 
   * @see ij.gui.DialogListener#dialogItemChanged(ij.gui.GenericDialog, java.awt.AWTEvent)
   */
  @Override
  public boolean dialogItemChanged(GenericDialog gd, AWTEvent e) {
    // fills options from UI
    if (gd.wasOKed()) {
      return true;
    }
    AnaOptions opts = (AnaOptions) options;
    // add and append results can not be both active
    {
      Checkbox cb4 = (Checkbox) gd.getCheckboxes().elementAt(4); // move results to table
      Checkbox cb5 = (Checkbox) gd.getCheckboxes().elementAt(5); // append results to table
      if (e.getSource() == cb4) {
        if (cb4.getState()) {
          cb5.setState(false);
        }
      }
      if (e.getSource() == cb5) {
        if (cb5.getState()) {
          cb4.setState(false);
        }
      }
    }

    Checkbox cb = (Checkbox) gd.getCheckboxes().elementAt(2); // clear measurements
    opts.clearFlu = cb.getState();
    Choice iob = (Choice) gd.getChoices().elementAt(0);
    if (opts.clearFlu && !anap.cleared) { // reset if clear measurments checked
      System.out.println("reset fluo");
      resetFluo();
      cb.setLabel("Measurments Cleared");
      IJ.log("All fluorescence measurements have been cleared");
      iob.select(0);
      return true;
    }

    opts.channel = gd.getNextChoiceIndex();
    opts.normalise = gd.getNextBoolean();
    opts.sampleAtSame = gd.getNextBoolean();
    opts.plotOutlines = ((Checkbox) gd.getCheckboxes().elementAt(3)).getState();
    // under multiple AAN run if there are many cells, remember only
    opts.fluoResultTable = ((Checkbox) gd.getCheckboxes().elementAt(4)).getState();
    opts.fluoResultTableAppend = ((Checkbox) gd.getCheckboxes().elementAt(5)).getState();
    // copy scale to macro options and configuration
    opts.userScale = gd.getNextNumber();
    anap.setCortextWidthScale(opts.userScale);
    if (anap.cleared) { // can't deselect
      cb.setState(true);
    }

    frameOneClone = (Outline) oh.indexGetOutline(0).clone(); // FIXME Change to copy constructor
    overlay.clear();
    drawOutlineAsOverlay(frameOneClone, Color.RED);
    shrink(frameOneClone);
    this.markFrozenNodesNormal(frameOneClone);
    setupImage.draw();
    drawOutlineAsOverlay(frameOneClone, Color.RED);
    return true;// gd.invalidNumber();
  }

  /**
   * Reset fluo.
   */
  void resetFluo() {
    // reset all fluo back to -2 and st res to 2 if only one frame
    AnaOptions opts = (AnaOptions) options;
    Outline o;
    for (int i = 0; i < oh.getSize(); i++) {
      o = oh.indexGetOutline(i);
      o.clearFluores();
      fluoStats[i].clearFluo();
    }

    if (oh.getSize() == 1) {
      // only one frame, so no ECMM. set outline res to 2
      System.out.println("Only one frame. set marker res to 2");
      oh.indexGetOutline(0).setResolution(anap.oneFrameRes);
    }

    // clear frame stats
    anap.noData = true;
    opts.channel = 0;
    anap.useLocFromCh = -1;
    anap.presentData[1] = 0;
    anap.presentData[2] = 0;
    anap.presentData[0] = 0;
    anap.fluTiffs[0] = new File("/");
    anap.fluTiffs[1] = new File("/");
    anap.fluTiffs[2] = new File("/");

    opts.channel = 0;
    anap.cleared = true;
  }

  /**
   * Sets the image scale.
   */
  void setImageScale() {
    setupImage.getCalibration().frameInterval = anap.frameInterval;
    setupImage.getCalibration().pixelHeight = anap.scale;
    setupImage.getCalibration().pixelWidth = anap.scale;
  }

  /**
   * Main method for fluorescence measurements analysis. Adds also new stats to FrameStatistics.
   * 
   * @see #runFromQconf()
   * @see #runFromPaqp()
   */
  private void runPlugin() {
    Roi outerRoi;
    Roi innerRoi;
    Outline o1;
    Outline s1;
    Outline s2;
    AnaOptions opts = (AnaOptions) options;
    IJ.showStatus("Running ANA (" + oh.getSize() + " frames)");
    for (int f = oh.getStartFrame(); f <= oh.getEndFrame(); f++) { // change i to frames
      IJ.log("Frame " + f);
      IJ.showProgress(f, oh.getEndFrame());

      setupImage.setSlice(f);
      o1 = oh.getStoredOutline(f);

      s1 = new Outline(o1);
      s2 = new Outline(o1);
      shrink(s2);

      // HACK for Du's embryoImage
      // shrink(s1);
      // s1.scale(14, 0.2);
      // ***

      // prepare overlay for current frame for plotting inner and outer outline
      overlay = new Overlay();
      setupImage.setOverlay(overlay);
      outerRoi = o1.asFloatRoi(); // convert outlines to ROI
      innerRoi = s2.asFloatRoi();
      outerRoi.setPosition(f); // set for frame f
      outerRoi.setStrokeColor(Color.BLUE);
      innerRoi.setPosition(f);
      innerRoi.setStrokeColor(Color.RED);

      // store in object, will be plotted depending on user choice.
      storedInnerROI.add(innerRoi);
      storedOuterROI.add(outerRoi);
      overlay.add(outerRoi); // this is for real time preview during computations
      overlay.add(innerRoi);

      Polygon polyS2 = s2.asPolygon();
      setFluoStats(s1.asPolygon(), polyS2, f); // compute FrameStatistics for frame f

      // compute Vert.fluores field in Outline (FluoMeasurement[] fluores)
      // use sample points already there
      if (opts.sampleAtSame && anap.useLocFromCh != -1) {
        useGivenSamplepoints(o1);
      } else {

        ecmH = new OutlineHandler(1, 2);
        ecmH.setOutline(1, s1);
        ecmH.setOutline(2, s2);

        ecmH = ecmMapping.runByANA(ecmH, orgIpr, anap.getCortexWidthPixel());

        // copy flur data to o1 and save
        // some nodes may fail to migrate properly so need to check
        // tracknumbers match
        Vert v = o1.getHead();
        Vert v2 = ecmH.getStoredOutline(2).getHead();

        while (v2.getTrackNum() != v.getTrackNum()) { // check id's match
          v = v.getNext();
          if (v.isHead()) {
            IJ.error("ANA fail");
            break;
            // return;
          }
        }

        int vertStart;
        do {
          v.setFluoresChannel(v2.fluores[0], opts.channel);
          v2 = v2.getNext();
          if (v2.isHead()) {
            break;
          }
          vertStart = v.getTrackNum();
          // find next vert in o1 that matches v2
          do {
            v = v.getNext();
            v.setFluoresChannel((int) Math.round(v.getX()), (int) Math.round(v.getY()), -1,
                    opts.channel); // map fail if -1. fix by interpolation
            if (vertStart == v.getTrackNum()) {
              System.out.println("ANA fail");
              return;
            }
          } while (v2.getTrackNum() != v.getTrackNum());
        } while (!v2.isHead());

        interpolateFailures(o1);
      }

      if (opts.normalise) {
        normalise2Interior(o1, f);
      }
      outputH.save(o1, f);
    }
  }

  private void shrink(Outline o) {
    // shrink outline
    o.scaleOutline(anap.getCortexWidthPixel(), -anap.stepRes, anap.angleTh, anap.freezeTh);

    o.unfreezeAll();
  }

  private void markFrozenNodesNormal(Outline o) {
    float[] x;
    float[] y;
    ExtendedVector2d norm;
    PolygonRoi pr;
    Vert v = o.getHead();
    do {
      if (v.isFrozen()) {
        overlay.setStrokeColor(Color.RED);
        norm = new ExtendedVector2d(v.getX(), v.getY());
        norm.addVec(v.getNormal());
        // norm.addVec(new Vect2d(1,1));

        x = new float[2];
        y = new float[2];

        x[0] = (float) v.getX();
        x[1] = (float) norm.getX();
        y[0] = (float) v.getY();
        y[1] = (float) norm.getY();
        pr = new PolygonRoi(x, y, 2, Roi.POLYGON);
        overlay.add(pr);
      }

      v = v.getNext();
    } while (!v.isHead());
  }

  /**
   * Compute statistics.
   * 
   * <p>Update {@link com.github.celldynamics.quimp.plugin.ana.ChannelStat} in
   * {@link com.github.celldynamics.quimp.FrameStatistics}
   * 
   * @param outerPoly outerPoly
   * @param innerPoly innerPoly
   * @param f frame
   */
  private void setFluoStats(Polygon outerPoly, Polygon innerPoly, int f) {
    AnaOptions opts = (AnaOptions) options;
    int store = f - anap.startFrame; // frame to index
    // System.out.println("store: " + store);
    fluoStats[store].frame = f;

    orgIpr.setRoi(outerPoly);
    // this does NOT scale to image
    ImageStatistics is = ImageStatistics.getStatistics(orgIpr, m, null);
    double outerAreaRaw = is.area;
    fluoStats[store].channels[opts.channel].totalFluor = is.mean * outerAreaRaw;
    fluoStats[store].channels[opts.channel].meanFluor = is.mean;

    orgIpr.setRoi(innerPoly);
    is = ImageStatistics.getStatistics(orgIpr, m, null);

    fluoStats[store].channels[opts.channel].innerArea =
            QuimpToolsCollection.areaToScale(is.area, anap.scale);
    fluoStats[store].channels[opts.channel].totalInnerFluor = is.mean * is.area;
    fluoStats[store].channels[opts.channel].meanInnerFluor = is.mean;

    fluoStats[store].channels[opts.channel].cortexArea =
            fluoStats[store].area - fluoStats[store].channels[opts.channel].innerArea; // scaled
    fluoStats[store].channels[opts.channel].totalCorFluo =
            fluoStats[store].channels[opts.channel].totalFluor
                    - fluoStats[store].channels[opts.channel].totalInnerFluor;
    fluoStats[store].channels[opts.channel].meanCorFluo =
            fluoStats[store].channels[opts.channel].totalCorFluo / (outerAreaRaw - is.area);

    fluoStats[store].channels[opts.channel].percCortexFluo =
            (fluoStats[store].channels[opts.channel].totalCorFluo
                    / fluoStats[store].channels[opts.channel].totalFluor) * 100;
    fluoStats[store].channels[opts.channel].cortexWidth = anap.getCortexWidthScale();
  }

  private void normalise2Interior(Outline o, int f) {
    AnaOptions opts = (AnaOptions) options;
    // interior mean fluorescence is used to normalse membrane measurments
    int store = f - anap.startFrame; // frame to index
    Vert v = o.getHead();
    do {
      v.fluores[opts.channel].intensity = v.fluores[opts.channel].intensity
              / fluoStats[store].channels[opts.channel].meanInnerFluor;
      v = v.getNext();
    } while (!v.isHead());

  }

  private void drawOutlineAsOverlay(Outline o, Color c) {
    Roi r = o.asFloatRoi();
    if (r.subPixelResolution()) {
      System.out.println("is sub pixel");
    } else {
      System.out.println("is not sub pixel");
    }
    overlay.setStrokeColor(c);
    overlay.add(r);
    setupImage.updateAndDraw();
  }

  private void investigateChannels(Outline o) {
    // flu maps
    int firstEmptyCh = -1;
    int firstFullCh = -1;
    AnaOptions opts = (AnaOptions) options;

    anap.presentData = new int[3];
    anap.noData = true;

    Vert v = o.getHead();
    for (int i = 0; i < 3; i++) {
      if (v.fluores[i].intensity == -2) { // no data
        anap.presentData[i] = 0;
        if (firstEmptyCh == -1) {
          firstEmptyCh = i;
        }
      } else {
        anap.presentData[i] = 1;
        IJ.log("Data exists in channel " + (i + 1));
        anap.noData = false;
        if (firstFullCh == -1) {
          firstFullCh = i;
        }
        // anap.setCortextWidthScale(fluoStats[0].channels[i].cortexWidth);
      }
    }

    if (QuimPArrayUtils.sumArray(anap.presentData) == 3) {
      firstEmptyCh = 0;
    }

    if (anap.noData) {
      opts.channel = 0;
      IJ.log("No previous sample points available.");
      anap.useLocFromCh = -1;
    } else {
      opts.channel = firstEmptyCh;
      IJ.log("Sample points from channel " + (firstFullCh + 1) + " available.");
      anap.useLocFromCh = firstFullCh;
    }

    v = o.getHead();
    for (int i = 0; i < 3; i++) {
      if (v.fluores[i].intensity != -2) {
        anap.setCortextWidthScale(fluoStats[0].channels[i].cortexWidth);
      }
    }
  }

  private void interpolateFailures(Outline o) {
    Vert v = o.getHead();
    Vert last;
    Vert nex;
    double disLtoN; // distance last to nex
    double disLtoV; // distance last to V
    double ratio;
    double intensityDiff;
    boolean fail;
    int firstID;
    AnaOptions opts = (AnaOptions) options;
    do {
      fail = false;
      if (v.fluores[opts.channel].intensity == -1) {
        IJ.log("\tInterpolated failed node intensity (position: " + v.coord + ")");
        // failed to map - interpolate with last/next successful

        last = v.getPrev();
        firstID = last.getTrackNum();
        while (last.fluores[opts.channel].intensity == -1) {
          last = last.getPrev();
          if (last.getTrackNum() == firstID) {
            IJ.log("Could not interpolate as all nodes failed");
            v.fluores[opts.channel].intensity = 0;
            fail = true;
          }
        }

        nex = v.getNext();
        firstID = nex.getTrackNum();
        while (nex.fluores[opts.channel].intensity == -1) {
          nex = nex.getNext();
          if (nex.getTrackNum() == firstID) {
            IJ.log("Could not interpolate as all nodes failed");
            v.fluores[opts.channel].intensity = 0;
            fail = true;
          }
        }

        if (fail) {
          v = v.getNext();
          continue;
        }

        disLtoN = ExtendedVector2d.lengthP2P(last.getPoint(), nex.getPoint());
        disLtoV = ExtendedVector2d.lengthP2P(last.getPoint(), v.getPoint());
        ratio = disLtoV / disLtoN;
        if (ratio > 1) {
          ratio = 1;
        }
        if (ratio < 0) {
          ratio = 0;
        }
        intensityDiff = (nex.fluores[opts.channel].intensity - last.fluores[opts.channel].intensity)
                * ratio;
        v.fluores[opts.channel].intensity = last.fluores[opts.channel].intensity + intensityDiff;
        if (v.fluores[opts.channel].intensity < 0 || v.fluores[opts.channel].intensity > 255) {
          IJ.log("Error. Interpolated intensity out of range. Set to zero.");
          v.fluores[opts.channel].intensity = 0;
        }
      }

      v = v.getNext();
    } while (!v.isHead());
  }

  private void drawSamplePointsFloat(Outline o, int frame) {
    float x;
    float y;
    PointRoi pr;
    AnaOptions opts = (AnaOptions) options;
    Vert v = o.getHead();
    do {
      x = (float) v.fluores[opts.channel].x;
      y = (float) v.fluores[opts.channel].y;
      pr = new PointRoi(x + 0.5, y + 0.5);
      pr.setPosition(frame);
      overlay.add(pr);
      v = v.getNext();
    } while (!v.isHead());
  }

  /**
   * Add fluorescence data to outline.
   * 
   * @param o1 outline to complete o1.fluores[channel] data
   */
  private void useGivenSamplepoints(Outline o1) {
    int x;
    int y;
    AnaOptions opts = (AnaOptions) options;
    Vert v = o1.getHead();
    do {
      x = (int) v.fluores[anap.useLocFromCh].x;
      y = (int) v.fluores[anap.useLocFromCh].y;
      // use the same sampling as for ECMM solving
      v.fluores[opts.channel].intensity = ODEsolver.sampleFluo(orgIpr, x, y);
      v.fluores[opts.channel].x = x;
      v.fluores[opts.channel].y = y;
      v = v.getNext();
    } while (!v.isHead());

  }
}