STmap.java

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

import java.io.File;
import java.io.IOException;

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

import com.github.celldynamics.quimp.Outline;
import com.github.celldynamics.quimp.OutlineHandler;
import com.github.celldynamics.quimp.QColor;
import com.github.celldynamics.quimp.QuimpException;
import com.github.celldynamics.quimp.Vert;
import com.github.celldynamics.quimp.filesystem.FileExtensions;
import com.github.celldynamics.quimp.filesystem.IQuimpSerialize;
import com.github.celldynamics.quimp.filesystem.converter.FormatConverter;
import com.github.celldynamics.quimp.geom.ExtendedVector2d;
import com.github.celldynamics.quimp.geom.filters.OutlineProcessor;
import com.github.celldynamics.quimp.utils.QuimPArrayUtils;
import com.github.celldynamics.quimp.utils.QuimpToolsCollection;

import ij.IJ;
import ij.ImagePlus;
import ij.process.ColorProcessor;
import ij.process.ImageProcessor;

/**
 * Create spatial temporal maps from ECMM and ANA data.
 * 
 * <p>This class can be serialized but only as container of maps. Data required for creation of
 * those maps are not serialized, thus restored object is not fully functional. As this is last step
 * in QuimP workflow it may not be necessary to load this JSON anymore.
 * 
 * @author rtyson
 * @author baniuk
 */
public class STmap implements IQuimpSerialize {

  /**
   * The Constant LOGGER. It is public because {@link FormatConverter#saveMaps(int)} can replace it
   */
  public static Logger LOGGER = LoggerFactory.getLogger(STmap.class.getName());

  /**
   * Motility map code.
   * 
   * @see #saveMaps(int)
   */
  public static final int MOTILITY = 1;
  /**
   * Convexity map code.
   * 
   * @see #saveMaps(int)
   */
  public static final int CONVEXITY = 2;
  /**
   * Origin map code.
   * 
   * @see #saveMaps(int)
   */
  public static final int ORIGIN = 4;
  /**
   * Coordinates map code.
   * 
   * @see #saveMaps(int)
   */
  public static final int COORD = 8;
  /**
   * X coordinates map code.
   * 
   * @see #saveMaps(int)
   */
  public static final int XMAP = 16;
  /**
   * Y coordinates map code.
   * 
   * @see #saveMaps(int)
   */
  public static final int YMAP = 32;
  /**
   * Fluorescence channel 1 map code.
   * 
   * @see #saveMaps(int)
   */
  public static final int FLU1 = 64;
  /**
   * Fluorescence channel 2 map code.
   * 
   * @see #saveMaps(int)
   */
  public static final int FLU2 = 128;
  /**
   * Fluorescence channel 3 map code.
   * 
   * @see #saveMaps(int)
   */
  public static final int FLU3 = 256;
  /**
   * Combine all FLU maps.
   * 
   * @see #saveMaps(int)
   */
  public static final int ALLFLU = FLU1 | FLU2 | FLU3;
  /**
   * Combine all maps.
   */
  public static final int ALLMAPS = MOTILITY | CONVEXITY | ORIGIN | COORD | XMAP | YMAP | ALLFLU;

  /**
   * Coordinates map.
   * 
   * <p>Each node has an associated position. The co-ordinate map, rather than contain values
   * regarding motility, fluorescence or convexity, instead contains the position values of nodes.
   * The main purpose of the co-ordinate map, along with the origin map, is for tracking positions
   * through time. Size is Map[getT()][getRes()]. Setter is recommended for proper initialisation.
   * 
   * @see <a href=
   *      "http://pilip.lnx.warwick.ac.uk/docs/develop/QuimP_Guide.html#x1-320005">Manual</a>
   */
  private double[][] coordMap;
  /**
   * Each node has an origin, the position a node originated from on the previous frame. The
   * origin map contains origin values and can be used, along with the co-ordinate map, to track
   * positions through time. Size is Map[getT()][getRes()]. Setter is recommended for proper
   * initialisation.
   * 
   * @see <a href=
   *      "http://pilip.lnx.warwick.ac.uk/docs/develop/QuimP_Guide.html#x1-320005">Manual</a>
   */
  private double[][] originMap;
  /**
   * Contains horizontal image pixel co-ordinates (those on the image used for segmentation)
   * relating to map pixels. Size is Map[getT()][getRes()]. Setter is recommended for proper
   * initialisation.
   * 
   * @see <a href=
   *      "http://pilip.lnx.warwick.ac.uk/docs/develop/QuimP_Guide.html#x1-320005">Manual</a>
   */
  private double[][] xMap;
  /**
   * Contains vertical image pixel co-ordinates (those on the image used for segmentation)
   * relating to map pixels. Size is Map[getT()][getRes()]. Setter is recommended for proper
   * initialisation.
   * 
   * @see <a href=
   *      "http://pilip.lnx.warwick.ac.uk/docs/develop/QuimP_Guide.html#x1-320005">Manual</a>
   */
  private double[][] yMap;
  /**
   * Motility map.
   * 
   * <p>Pixels are coloured according to node speed, as calculated by ECMM. Red shades represent
   * expanding regions, blue shades contracting regions. Pixel values within the tiff image are
   * scaled to fill the colour spectrum. The map file extended _motilityMap.maPQ contains
   * un-scaled values, in microns per second. Size is Map[getT()][getRes()]. Setter is recommended
   * for proper initialisation.
   * 
   * @see <a href=
   *      "http://pilip.lnx.warwick.ac.uk/docs/develop/QuimP_Guide.html#x1-320005">Manual</a>
   */
  private double[][] motMap;

  /**
   * The mig color.
   */
  transient int[] migColor;

  /**
   * The mig pixels.
   */
  transient float[] migPixels;
  /**
   * Fluoroscence maps for channels.
   */
  public FluoMap[] fluoMaps;
  /**
   * Convexity map.
   * 
   * <p>Size is Map[getT()][getRes()]. Setter is recommended for proper initialisation.
   */
  private double[][] convMap;

  /**
   * The conv color.
   */
  transient int[] convColor;

  /**
   * The conv im P.
   */
  transient ImagePlus migImP;
  transient ImagePlus fluImP;
  transient ImagePlus convImP;
  /**
   * Contain OutlineHandler used for generating maps.
   * 
   * <p><b>warning</b>
   * 
   * <p>It is not serialized
   */
  transient OutlineHandler oh;
  /**
   * Resolution of maps.
   * 
   * <p>This field together with <tt>T</tt> stands for the dimensions of 2D arrays for storing maps.
   * For this reason they are serialized. Map[T][res]
   */
  private int res;
  /**
   * Number of timeframes.
   */
  private int T;
  private double mapPixelHeight = 1;
  private double mapPixelWidth = 1;

  private transient Qp params; // configuration parameters

  /**
   * Default constructor to satisfy GSon builder. Should not be used for proper object
   * Initialisation.
   */
  public STmap() {
    this.fluoMaps = new FluoMap[3];
    this.params = new Qp();
  }

  /**
   * Copy constructor.
   * 
   * <p><b>warning</b>
   * 
   * <p>Make a copy of serializable fields only
   * 
   * @param src source object
   */
  public STmap(final STmap src) {
    this();
    this.coordMap = QuimPArrayUtils.copy2darray(src.coordMap, null);
    this.originMap = QuimPArrayUtils.copy2darray(src.originMap, null);
    this.xMap = QuimPArrayUtils.copy2darray(src.xMap, null);
    this.yMap = QuimPArrayUtils.copy2darray(src.yMap, null);
    this.motMap = QuimPArrayUtils.copy2darray(src.motMap, null);
    this.convMap = QuimPArrayUtils.copy2darray(src.convMap, null);
    this.res = src.res;
    this.T = src.T;
    this.mapPixelHeight = src.mapPixelHeight;
    this.mapPixelWidth = src.mapPixelWidth;
    this.fluoMaps = new FluoMap[src.fluoMaps.length];
    for (int i = 0; i < src.fluoMaps.length; i++) {
      this.fluoMaps[i] = new FluoMap(src.fluoMaps[i]);
    }
    try {
      this.params = (Qp) src.params.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace(); // should not happen
    }

  }

  /**
   * Build object for given.
   * 
   * @param o Outline from ECMM
   * @param r Map resolution in pixels
   * @param params configuration of Qanalysis
   * @see com.github.celldynamics.quimp.plugin.qanalysis.Qp
   */
  public STmap(OutlineHandler o, int r, Qp params) {
    this();
    this.params = params;
    mapPixelHeight = 1;
    mapPixelWidth = 1.0d / r;
    res = r;
    oh = o;
    T = oh.getSize();

    coordMap = new double[T][res];
    originMap = new double[T][res];
    xMap = new double[T][res]; // interpolated pixel coordinates
    yMap = new double[T][res];

    motMap = new double[T][res];
    migColor = new int[T * res];
    migPixels = new float[T * res];

    // fluMap = new double[T][res];
    // fluColor = new byte[T * res];

    convMap = new double[T][res];
    convColor = new int[T * res];

    // flu maps
    Vert v = oh.indexGetOutline(0).getHead();
    for (int i = 0; i < 3; i++) {
      fluoMaps[i] = new FluoMap(T, res, i + 1);
      System.out.println("flou in v: " + v.fluores[i].intensity);
      if (v.fluores[i].intensity == -2) { // disable if no data
        IJ.log("No fluorescence data for channel " + (i + 1));
        fluoMaps[i].setEnabled(false);
      }
    }

    generate();
    saveConvMotImages(); // save images that are opened already
  }

  /**
   * Generate all maps saved by Q Analysis Fill internal class fields.
   */
  private void generate() {

    this.calcCurvature();
    Vert zeroVert;
    Vert v;
    String migColorMap = "rwb";

    double fraction;
    double intMig;
    double intFlu;
    double intConv;
    double target;
    double actualTarget;
    QColor color;
    int pn;
    Vert fhead;
    Vert chead;

    double step = 1.0d / res;

    // ------debug----
    // System.out.println("210.in generate: min:"+ oh.migLimits[0]+",
    // max"+oh.migLimits[1]);
    // zeroVert = closestFloor(oh.getOutline(28), 0.9138373074502235, 'f');
    // fHead = findFirstNode(oh.getOutline(28),'f');
    // cHead = findFirstNode(oh.getOutline(28),'c');
    // if(true)return;
    // -------------------------

    double origin = 0; // co-ord for zeroVert to move to next
    int frame;

    for (int tt = 0; tt < T; tt++) {

      frame = tt + oh.getStartFrame();
      // System.out.println("frame " + t);
      pn = tt * res; // pixel index

      // find the first node in terms of coord and fcoord (not the head)
      fhead = oh.getStoredOutline(frame).findFirstNode('f');
      chead = oh.getStoredOutline(frame).findFirstNode('c');
      // fHead.print();
      // cHead.print();

      if (tt == 0) {
        // for the first time point the head coord node is our starting point
        zeroVert = chead;
        fraction = 0;
        origin = 0;
      } else {
        // vert closest below zero (zero being tracked over time from
        // frame 1!)
        zeroVert = closestFloor(oh.getStoredOutline(frame), origin, 'f', fhead);
        // System.out.println("zerovert: " + zeroVert.fCoord +", origin:
        // " + origin + ", fHead: " + fHead.fCoord);
        // position of origin between zeroVert and zeroVert.getNext
        fraction = ffraction(zeroVert, origin, fhead);
        // System.out.println("resulting fraction: " + fraction);

        // System.out.print("\nzeroVert.fCoord:"+zeroVert.coord+",
        // fraction:"+fraction +"\n");
        origin = interpCoord(zeroVert, fraction, chead); // the new origin
        // System.out.println("new origin: " + origin);
      }
      target = origin; // coord to fill in map next

      intMig = interpolate(zeroVert.distance, zeroVert.getNext().distance, fraction);
      motMap[tt][0] = intMig;
      color = QColor.erColorMap2(migColorMap, intMig, oh.migLimits[0], oh.migLimits[1]);
      migColor[pn] = color.getColorInt();
      migPixels[pn] = (float) intMig;

      // fill fluo maps
      for (int i = 0; i < 3; i++) {
        if (fluoMaps[i].isEnabled()) {
          if (zeroVert.fluores[i].intensity == -2) {
            IJ.log("ERROR: There are missing fluoresecne values! Run ANA");
            return;
          }
          intFlu = interpolate(zeroVert.fluores[i].intensity,
                  zeroVert.getNext().fluores[i].intensity, fraction);
          fluoMaps[i].fill(tt, 0, pn, intFlu, oh.fluLims[i][1]);
        }
      }

      /*
       * if (zeroVert.floures == -1) { fluMap[t][0] = 0; fluColor[pN] = (byte)
       * QColor.bwScale(0, 256, oh.maxFlu, 0); } else { intFlu = interpolate(zeroVert.floures,
       * zeroVert.getNext().floures, fraction); fluMap[t][0] = intFlu; fluColor[pN] = (byte)
       * QColor.bwScale(intFlu, 256, oh.maxFlu, 0); }
       */

      intConv = interpolate(zeroVert.curvatureSum, zeroVert.getNext().curvatureSum, fraction);
      convMap[tt][0] = intConv;
      color = QColor.erColorMap2("rbb", intConv, oh.curvLimits[0], oh.curvLimits[1]);
      convColor[pn] = color.getColorInt();

      coordMap[tt][0] = origin;
      originMap[tt][0] = interpFCoord(zeroVert, fraction, fhead);
      xMap[tt][0] = interpolate(zeroVert.getX(), zeroVert.getNext().getX(), fraction);
      yMap[tt][0] = interpolate(zeroVert.getY(), zeroVert.getNext().getY(), fraction);

      if (target >= 1 || target < 0) {
        System.out.println("target out of range: " + target);
      }

      for (int p = 1; p < res; p++) {
        pn = (tt * res) + p; // pixel index
        target += step;
        actualTarget = (target >= 1) ? target - 1 : target; // wraps around to zero
        // System.out.println("\tactualtarget:"+actualTarget);
        coordMap[tt][p] = actualTarget;

        v = closestFloor(oh.getStoredOutline(frame), actualTarget, 'c', chead); // should this be g
        fraction = cfraction(v, actualTarget, chead);

        originMap[tt][p] = interpFCoord(v, fraction, fhead);
        xMap[tt][p] = interpolate(v.getX(), v.getNext().getX(), fraction);
        yMap[tt][p] = interpolate(v.getY(), v.getNext().getY(), fraction);

        intMig = interpolate(v.distance, v.getNext().distance, fraction);
        motMap[tt][p] = intMig;
        color = QColor.erColorMap2(migColorMap, intMig, oh.migLimits[0], oh.migLimits[1]);
        migColor[pn] = color.getColorInt();
        migPixels[pn] = (float) intMig;

        for (int i = 0; i < 3; i++) {
          if (fluoMaps[i].isEnabled()) {
            intFlu = interpolate(v.fluores[i].intensity, v.getNext().fluores[i].intensity,
                    fraction);
            fluoMaps[i].fill(tt, p, pn, intFlu, oh.fluLims[i][1]);
          }
        }
        /*
         * if (zeroVert.floures == -1) { fluMap[t][p] = 0; fluColor[pN] = (byte)
         * QColor.bwScale(0, 256, oh.maxFlu, 0); } else { intFlu = interpolate(v.floures,
         * v.getNext().floures, fraction); fluMap[t][p] = intFlu; fluColor[pN] = (byte)
         * QColor.bwScale(intFlu, 256, oh.maxFlu, 0); }
         */

        intConv = interpolate(v.curvatureSum, v.getNext().curvatureSum, fraction);
        convMap[tt][p] = intConv;
        color = QColor.erColorMap2("rbb", intConv, oh.curvLimits[0], oh.curvLimits[1]);
        convColor[pn] = color.getColorInt();

      }

    }

    migImP = map2ImagePlus("motility_map", new ColorProcessor(res, T, migColor));
    convImP = map2ImagePlus("convexity_map", new ColorProcessor(res, T, convColor));
    migImP.show();
    convImP.show();
    // fluImP.show();
    // IJ.doCommand("Red");

    if (params.Build3D) {
      // create 3D of motility
      STMap3D map3d = new STMap3D(xMap, yMap, migColor);
      map3d.build();
      map3d.toOrigin(oh.indexGetOutline(0).getCentroid());
      map3d.scale(0.05f);
      map3d.write(new File("/tmp/cell_02.wrl"));

      // create 3D of curvature
      STMap3D map3dCur = new STMap3D(xMap, yMap, convColor);
      map3dCur.build();
      map3dCur.toOrigin(oh.indexGetOutline(0).getCentroid());
      map3dCur.scale(0.05f);
      map3dCur.write(new File("/tmp/cell_02_cur.wrl"));
    }

    // create fluo images

    for (int i = 0; i < 3; i++) {
      if (!fluoMaps[i].isEnabled()) {
        continue;
      }

      fluImP = IJ.createImage(params.filename + "_fluoCH" + fluoMaps[i].getChannel(), "8-bit black",
              res, T, 1);
      fluImP.getProcessor().setPixels(fluoMaps[i].getColours());
      resize(fluImP);
      setCalibration(fluImP);
      fluImP.show();

      try {
        Thread.sleep(500); // needed to let imageJ set the right colour maps
      } catch (Exception e) {
        ;
      }

      IJ.doCommand("Red"); // this don't always work. dun know why
      String tmpfilename = FileExtensions.fluomapFileExt.replaceFirst("%",
              Integer.toString(fluoMaps[i].getChannel()));
      IJ.saveAs(fluImP, "tiff",
              params.outFile.getParent() + File.separator + params.filename + tmpfilename);
    }

    // saveMaps(); // save maQP files

    if (QuimPArrayUtils.sumArray(migColor) == 0) {
      IJ.showMessage("ECMM data is missing (or corrupt), and is needed for building accurate maps.+"
              + "\nPlease run ECMM (fluorescence data will be lost)");
    }
    // test making LUT images
    /*
     * ImagePlus migImPLut = IJ.createImage("mig_32", "32-bit", res, T,1); ImageProcessor
     * ipFloat = new FloatProcessor(res, T, migPixels, null); LUT lut = new LUT();
     * ipFloat.setLut(lut) migImPLut.setProcessor(ipFloat); resize(migImPLut); migImPLut.show();
     */
  }

  /**
   * Save map files (maQP) on disk.
   * 
   * @param maps Map to be saved, defined in this class. Use {@link STmap#ALLMAPS} for save all
   * @throws QuimpException any error with saving maps (except IO)
   */
  public void saveMaps(int maps) throws QuimpException {
    try {
      if ((maps & MOTILITY) == MOTILITY) {
        File f = new File(params.outFile.getPath() + FileExtensions.motmapFileExt);
        QuimPArrayUtils.arrayToFile(motMap, ",", f);
        LOGGER.info("\tSaved motility map at: " + f.getAbsolutePath());
      }
      if ((maps & CONVEXITY) == CONVEXITY) {
        File f = new File(params.outFile.getPath() + FileExtensions.convmapFileExt);
        QuimPArrayUtils.arrayToFile(convMap, ",", f);
        LOGGER.info("\tSaved convexity map at: " + f.getAbsolutePath());
      }
      if ((maps & ORIGIN) == ORIGIN) {
        File f = new File(params.outFile.getPath() + FileExtensions.originmapFileExt);
        QuimPArrayUtils.arrayToFile(originMap, ",", f);
        LOGGER.info("\tSaved origin map at: " + f.getAbsolutePath());
      }
      if ((maps & COORD) == COORD) {
        File f = new File(params.outFile.getPath() + FileExtensions.coordmapFileExt);
        QuimPArrayUtils.arrayToFile(coordMap, ",", f);
        LOGGER.info("\tSaved coord map at: " + f.getAbsolutePath());
      }
      if ((maps & XMAP) == XMAP) {
        File f = new File(params.outFile.getPath() + FileExtensions.xmapFileExt);
        QuimPArrayUtils.arrayToFile(xMap, ",", f);
        LOGGER.info("\tSaved x map at: " + f.getAbsolutePath());
      }
      if ((maps & YMAP) == YMAP) {
        File f = new File(params.outFile.getPath() + FileExtensions.ymapFileExt);
        QuimPArrayUtils.arrayToFile(yMap, ",", f);
        LOGGER.info("\tSaved y map at: " + f.getAbsolutePath());
      }
      if ((maps & FLU1) == FLU1) {
        saveFluoroMap(0);
      }
      if ((maps & FLU2) == FLU2) {
        saveFluoroMap(1);
      }
      if ((maps & FLU3) == FLU3) {
        saveFluoroMap(2);
      }
    } catch (NullPointerException np) {
      LOGGER.debug(np.getMessage(), np);
      throw new QuimpException("Can not save map. Input array does not exist: " + np.getMessage());
    } catch (IOException e1) {
      IJ.error("Could not write Map file:\n " + e1.getMessage());
      LOGGER.debug(e1.getMessage(), e1);
      throw new QuimpException(e1);
    } catch (Exception e) {
      LOGGER.debug(e.getMessage(), e);
      throw new QuimpException(e);
    }
  }

  /**
   * Saves selected fluoro map.
   * 
   * @param index map index in {@link #fluoMaps} array.
   * 
   * @throws IOException on file save
   */
  private void saveFluoroMap(int index) throws IOException {
    if (!fluoMaps[index].isEnabled()) {
      LOGGER.debug("Selected map " + (index + 1) + " is not enabled");
    } else {
      String tmpfilename = FileExtensions.fluomapFileExt.replaceFirst("%",
              Integer.toString(fluoMaps[index].getChannel()));
      File f = new File(params.outFile.getPath() + tmpfilename);
      QuimPArrayUtils.arrayToFile(fluoMaps[index].getMap(), ",", f);
      LOGGER.info("\tSaved fluoro map at: " + f.getAbsolutePath());
    }
  }

  /**
   * Saves already opened generated motility and convexity images.
   */
  private void saveConvMotImages() {
    // save images
    IJ.saveAs(migImP, "tiff", params.outFile.getParent() + File.separator + params.filename
            + FileExtensions.motimageFileExt);
    IJ.saveAs(convImP, "tiff", params.outFile.getParent() + File.separator + params.filename
            + FileExtensions.convimageFileExt);
  }

  /**
   * Convert ImageProcessor created from Map to ImagePlus.
   * 
   * <p>Usually maps are non-square and need to be rescaled for correct presentation, what this
   * method does.
   * 
   * <p><b>Note</b>
   * 
   * <p>Take care about what <tt>ImageProcessor</tt> is used. The <tt>ColorProcessor</tt> is created
   * from 1D array, whereas e.g. <tt>FloatProcessor</tt> from 2D arrays. This causes that the same
   * map will be displayed with different orientation. QuimP natively uses maps presented by
   * <tt>ColorProcessor</tt>, if one uses <tt>FloatProcessor</tt> it must be rotated and flip to
   * maintain correct orientation. See the following example:
   * 
   * <pre>
   * {
   *   &#64;code
   *   float[][] motMap = QuimPArrayUtils.double2float(mapCell.motMap);
   *   ImageProcessor imp = new FloatProcessor(motMap).rotateRight();
   *   imp.flipHorizontal();
   *   mapCell.map2ImagePlus("motility_map", imp).show();
   * }
   * </pre>
   * 
   * @param name Name of the ImagePlus window
   * @param imp Image processor
   * @return Created ImagePlus object
   * @see #map2ColorImagePlus(String, String, double[][], double, double)
   */
  public ImagePlus map2ImagePlus(String name, ImageProcessor imp) {
    ImagePlus ret = new ImagePlus(name, imp);
    resize(ret);
    setCalibration(ret);
    return ret;
  }

  /**
   * Convert raw map to colorscale.
   * 
   * <p><b>warning</b>
   * 
   * <p>Assumes that input 2D array map is regular and not column. This method can be used with data
   * restored from <i>QCONF</i> file in the following way:
   * 
   * <pre>
   * {@code
   * // Maps are correlated in order with Outlines in DataContainer.
   * mapCell.map2ColorImagePlus("motility_map", "rwb",mapCell.motMap, oHs.oHs.get(h).migLimits[0],
   *               oHs.oHs.get(h).migLimits[1]).show();
   * }
   * </pre>
   * 
   * @param name Name of the ImagePlus
   * @param map Map to convert
   * @param palette Palette code, can be "rww" or "rwb".
   * @param min Minimum value in Outline that was used for creation of this map
   * @param max Maximum value in Outline that was used for creation of this map
   * @return Created ImagePlus object
   * @see #map2ImagePlus(String, ImageProcessor)
   * @see QColor#erColorMap2(String, double, double, double) for palettes
   */
  public ImagePlus map2ColorImagePlus(String name, String palette, double[][] map, double min,
          double max) {
    int[] migColor = new int[map.length * map[0].length];
    int pn = 0;
    int t = map.length;
    int res = map[0].length;
    for (int r = 0; r < t; r++) {
      for (int c = 0; c < res; c++) {
        // pN = (c * mapCell.getT()) + r;
        QColor color = QColor.erColorMap2(palette, map[r][c], min, max);
        migColor[pn++] = color.getColorInt();
      }
    }
    return map2ImagePlus(name, new ColorProcessor(res, t, migColor));
  }

  private Vert closestFloor(Outline o, double target, char c, Vert head) {
    // find the vert with coor closest (floored) to target coordinate

    Vert v = head; // the fcoord or cCoord head
    double coord;
    double coordNext;
    do {
      coord = (c == 'f') ? v.fCoord : v.coord;
      coordNext = (c == 'f') ? v.getNext().fCoord : v.getNext().coord;

      if (coord == target) {
        break;
      }
      if (coordNext > target && coord < target) {
        break;
      }

      v = v.getNext();
    } while (v.getNext().getTrackNum() != head.getTrackNum());

    // System.out.println("found fcoord " + v.fCoord);
    return v;
  }

  private double cfraction(Vert v, double target, Vert head) {
    // calc fraction for iterpolation
    double v2coord;
    if (v.getNext().getTrackNum() == head.getTrackNum()) { // passed zero
      v2coord = v.getNext().coord + 1;
      target = (target > v.coord) ? target : target + 1; // not passed zero as all values are zero!
    } else {
      v2coord = v.getNext().coord;
    }

    double frac = (target - v.coord) / (v2coord - v.coord);
    // System.out.println("\tffraction:
    // |v:"+v.coord+"|v2:"+v2coord+"|tar:"+target+"|frac:"+frac);
    if (frac >= 1) {
      frac = frac - 1;
      LOGGER.warn("WARNING- frac corrected: " + frac);
    }
    if (frac > 1 || frac < 0) {
      LOGGER.warn("!WARNING, frac out of range:" + frac);
    }
    return frac;
  }

  private double ffraction(Vert v, double target, Vert head) {
    // calc fraction for iterpolation
    double v2coord;
    if (v.getNext().getTrackNum() == head.getTrackNum()) { // passed zero
      // System.out.println("\tffraction: pass zero. wrap");
      v2coord = v.getNext().fCoord + 1;
      target = (target > v.fCoord) ? target : target + 1; // not passed zero as all values are zero!
    } else {
      v2coord = v.getNext().fCoord;
    }
    double frac = (target - v.fCoord) / (v2coord - v.fCoord);
    // System.out.println("\tffraction:
    // |v:"+v.fCoord+"|v2:"+v2coord+"|tar:"+target+"|frac:"+frac);

    if (frac >= 1) {
      frac = frac - 1;
      LOGGER.warn("WARNING- frac corrected: " + frac);
    }
    if (frac > 1 || frac < 0) {
      LOGGER.warn("WARNING, frac out of range:" + frac);
    }

    if (Double.isNaN(frac)) {
      LOGGER.warn("WARNING, frac is nan:" + frac);
      System.out.println("\tffraction: |v:" + v.fCoord + "|v2:" + v2coord + "|tar:" + target
              + "|frac:" + frac);
      frac = 0.5;
    }
    return frac;
  }

  private double interpCoord(Vert v, double frac, Vert head) {
    double v2Coord = (v.getNext().getTrackNum() == head.getTrackNum()) ? v.getNext().coord + 1
            : v.getNext().coord; // pass zero
    double dis = v2Coord - v.coord;
    double targ = v.coord + (dis * frac);

    if (targ >= 1) {
      targ += -1; // passed zero
    }

    if (targ < 0) {
      LOGGER.error("ERROR: target less than zero");
    }

    return targ;
  }

  private double interpFCoord(Vert v, double frac, Vert head) {
    double v2Coord = (v.getNext().getTrackNum() == head.getTrackNum()) ? v.getNext().fCoord + 1
            : v.getNext().fCoord;
    double dis = v2Coord - v.fCoord;
    double targ = v.fCoord + (dis * frac);

    if (targ >= 1) {
      targ += -1; // passed zero
    }

    if (targ < 0) {
      LOGGER.error("ERROR: target less than zero");
    }

    return targ;
  }

  private double interpolate(double v1, double v2, double frac) {
    return v1 + ((v2 - v1) * frac);
  }

  /**
   * Calculates convexity by smoothing or averaging across nodes.
   */
  private void calcCurvature() {

    Outline o;
    Vert v;

    oh.curvLimits = new double[2];

    for (int f = oh.getStartFrame(); f <= oh.getEndFrame(); f++) {
      o = oh.getStoredOutline(f);
      if (o == null) {
        IJ.log("ERROR: Outline at frame " + f + " is empty");
        continue;
      }

      // update local curvature just in case
      o.updateCurvature();

      // set default curvatures
      v = o.getHead();
      do {
        v.curvatureSmoothed = v.curvatureLocal;
        v.curvatureSum = v.curvatureLocal;
        v = v.getNext();
      } while (!v.isHead());

      new OutlineProcessor<Outline>(o).averageCurvature(params.avgCov).sumCurvature(params.sumCov);
      // averageCurvature(o);
      // sumCurvature(o);

      // find min and max of sum curvature

      v = o.getHead();
      if (f == oh.getStartFrame()) {
        oh.curvLimits[1] = v.curvatureSum;
        oh.curvLimits[0] = v.curvatureSum;
      }
      do {
        if (v.curvatureSum > oh.curvLimits[1]) {
          oh.curvLimits[1] = v.curvatureSum;
        }
        if (v.curvatureSum < oh.curvLimits[0]) {
          oh.curvLimits[0] = v.curvatureSum;
        }
        v = v.getNext();
      } while (!v.isHead());
      // System.out.println("Min curv: " + oh.curvLimits[0] + ", max curv:
      // " + oh.curvLimits[1]);

    }

    // System.out.println("curve limits before: " + oh.curvLimits[0] + ", "
    // + oh.curvLimits[1]);
    oh.curvLimits = QuimpToolsCollection.setLimitsEqual(oh.curvLimits);
    // System.out.println("curve limits after: " + oh.curvLimits[0] + ", " +
    // oh.curvLimits[1]);
  }

  @Deprecated
  private void averageCurvature(Outline o) {

    Vert v;
    Vert tmpV;
    double totalCur;
    double distance;
    int count;

    // avertage over curvatures
    if (params.avgCov > 0) {
      // System.out.println("new outline");
      v = o.getHead();
      do {
        // System.out.println("\tnew vert");
        totalCur = v.curvatureLocal; // reset
        count = 1;

        // add up curvatures of prev nodes
        // System.out.println("\t prev nodes");
        tmpV = v.getPrev();
        distance = 0;
        do {
          distance += ExtendedVector2d.lengthP2P(tmpV.getNext().getPoint(), tmpV.getPoint());
          totalCur += tmpV.curvatureLocal;
          count++;
          tmpV = tmpV.getPrev();
        } while (distance < params.avgCov / 2);

        // add up curvatures of next nodes
        distance = 0;
        tmpV = v.getNext();
        do {
          distance += ExtendedVector2d.lengthP2P(tmpV.getPrev().getPoint(), tmpV.getPoint());
          totalCur += tmpV.curvatureLocal;
          count++;
          tmpV = tmpV.getNext();
        } while (distance < params.avgCov / 2);

        v.curvatureSmoothed = totalCur / count;

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

  /**
   * Sum smoothed curavture over a region of the membrane.
   * 
   * @param o the outline
   */
  @Deprecated
  private void sumCurvature(Outline o) {

    Vert v;
    Vert tmpV;
    double totalCur;
    double distance;
    // avertage over curvatures
    if (params.sumCov > 0) {
      LOGGER.trace("summing curv");
      v = o.getHead();
      do {
        // System.out.println("\tnew vert");
        totalCur = v.curvatureSmoothed; // reset
        // add up curvatures of prev nodes
        // System.out.println("\t prev nodes");
        tmpV = v.getPrev();
        distance = 0;
        do {
          distance += ExtendedVector2d.lengthP2P(tmpV.getNext().getPoint(), tmpV.getPoint());
          totalCur += tmpV.curvatureSmoothed;
          tmpV = tmpV.getPrev();
        } while (distance < params.sumCov / 2);

        // add up curvatures of next nodes
        distance = 0;
        tmpV = v.getNext();
        do {
          distance += ExtendedVector2d.lengthP2P(tmpV.getPrev().getPoint(), tmpV.getPoint());
          totalCur += tmpV.curvatureSmoothed;
          tmpV = tmpV.getNext();
        } while (distance < params.sumCov / 2);

        v.curvatureSum = totalCur;

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

  }

  /**
   * Compute vertical resolution tiff image generated from map.
   * 
   * <p>Horizontal resolution is as defined in UI but vertical can be different. USe this method
   * if input coordinates comes from image but not map.
   * 
   * @return vertical resolution.
   * @see STmap#resize(ImagePlus)
   */
  public int getVerticalResolution() {
    double vertRes = Math.ceil((double) res / (double) T);
    return (int) (T * vertRes);
  }

  /**
   * Resize image to size of this map.
   * 
   * @param imp image to resize.
   */
  public void resize(ImagePlus imp) {
    if (T >= res * 0.9) {
      return; // don't resize if its going to compress frames
    }
    ImageProcessor ip = imp.getProcessor();

    ip.setInterpolationMethod(ImageProcessor.NONE);

    double vertRes = Math.ceil((double) res / (double) T);
    mapPixelHeight = 1.0d / vertRes;
    vertRes = T * vertRes;

    ip = ip.resize(res, (int) vertRes);
    imp.setProcessor(ip);

  }

  /**
   * Calibrate image for sizes of this maps.
   * 
   * @param imp image to calibrate.
   */
  public void setCalibration(ImagePlus imp) {
    imp.getCalibration().setUnit("frames");
    imp.getCalibration().pixelHeight = mapPixelHeight;
    imp.getCalibration().pixelWidth = mapPixelWidth;
    LOGGER.debug("PixelWidth=" + mapPixelWidth + " PixelHeight=" + mapPixelHeight);
  }

  /**
   * Return resolution of the map.
   * 
   * <p>This is value set by user in Q Analysis UI.
   * 
   * @return the res
   */
  public int getRes() {
    return res;
  }

  /**
   * Return number of outline points.
   * 
   * <p>This is value obtained after interpolation of Outlines.
   * 
   * @return the T
   */
  public int getT() {
    return T;
  }

  /**
   * getxMap.
   * 
   * @return the xMap
   */
  public double[][] getxMap() {
    return xMap;
  }

  /**
   * Set map. Initialise also resolution fields. All maps must have the same resolution.
   * 
   * @param xmap the xMap to set
   * @see STmap#getRes()
   * @see #getT()
   */
  public void setxMap(double[][] xmap) {
    this.xMap = xmap;
    T = xmap.length;
    res = xmap[0].length;
  }

  /**
   * getyMap.
   * 
   * @return the yMap
   */
  public double[][] getyMap() {
    return yMap;
  }

  /**
   * Set map. Initialise also resolution fields. All maps must have the same resolution.
   * 
   * @param ymap the yMap to set
   * @see STmap#getRes()
   * @see #getT()
   */
  public void setyMap(double[][] ymap) {
    this.yMap = ymap;
    T = ymap.length;
    res = ymap[0].length;
  }

  /**
   * Get coordinates map.
   * 
   * @return the coordMap
   */
  public double[][] getCoordMap() {
    return coordMap;
  }

  /**
   * Set map. Initialise also resolution fields. All maps must have the same resolution.
   * 
   * @param coordMap the coordMap to set
   * @see STmap#getRes()
   * @see #getT()
   */
  public void setCoordMap(double[][] coordMap) {
    this.coordMap = coordMap;
    T = coordMap.length;
    res = coordMap[0].length;
  }

  /**
   * Get origin map.
   * 
   * @return the originMap
   */
  public double[][] getOriginMap() {
    return originMap;
  }

  /**
   * Set map. Initialise also resolution fields. All maps must have the same resolution.
   * 
   * @param originMap the originMap to set
   * 
   * @see STmap#getRes()
   * @see #getT()
   */
  public void setOriginMap(double[][] originMap) {
    this.originMap = originMap;
    T = originMap.length;
    res = originMap[0].length;
  }

  /**
   * getMotMap.
   * 
   * @return the motMap
   */
  public double[][] getMotMap() {
    return motMap;
  }

  /**
   * Set map. Initialise also resolution fields. All maps must have the same resolution.
   * 
   * @param motMap the motMap to set
   * 
   * @see STmap#getRes()
   * @see #getT()
   */
  public void setMotMap(double[][] motMap) {
    this.motMap = motMap;
    T = motMap.length;
    res = motMap[0].length;
  }

  /**
   * getConvMap.
   * 
   * @return the convMap
   */
  public double[][] getConvMap() {
    return convMap;
  }

  /**
   * getFluMaps.
   * 
   * @return the fluo maps
   */
  public FluoMap[] getFluMaps() {
    return fluoMaps;
  }

  /**
   * Set map. Initialise also resolution fields. All maps must have the same resolution.
   * 
   * @param convMap the convMap to set
   * @see STmap#getRes()
   * @see #getT()
   */
  public void setConvMap(double[][] convMap) {
    this.convMap = convMap;
    T = convMap.length;
    res = convMap[0].length;
  }

  /**
   * Attach other than default configuration object.
   * 
   * <p>This is passed by constructors, use {@link #setParams(Qp)} if deserialised.
   * 
   * @param params the params to set
   */
  public void setParams(Qp params) {
    this.params = params;
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.filesystem.IQuimpSerialize#beforeSerialize()
   */
  @Override
  public void beforeSerialize() {
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.filesystem.IQuimpSerialize#afterSerialize()
   */
  @Override
  public void afterSerialize() throws Exception {
    LOGGER.debug("This class can not be deserialzied without assgning OutlineHndler and Qp");
  }
}