View Javadoc
1   package com.github.celldynamics.quimp.geom.filters;
2   
3   import java.io.FileWriter;
4   import java.io.IOException;
5   import java.io.PrintWriter;
6   import java.nio.file.Path;
7   import java.nio.file.Paths;
8   import java.util.ArrayList;
9   import java.util.Collections;
10  import java.util.List;
11  import java.util.TreeSet;
12  import java.util.stream.Collectors;
13  import java.util.stream.IntStream;
14  
15  import org.apache.commons.lang3.tuple.MutablePair;
16  import org.apache.commons.lang3.tuple.Pair;
17  import org.scijava.vecmath.Point2d;
18  import org.scijava.vecmath.Tuple2d;
19  import org.scijava.vecmath.Vector2d;
20  import org.slf4j.Logger;
21  import org.slf4j.LoggerFactory;
22  
23  import com.github.celldynamics.quimp.Outline;
24  import com.github.celldynamics.quimp.QuimP;
25  import com.github.celldynamics.quimp.geom.BasicPolygons;
26  import com.github.celldynamics.quimp.plugin.QuimpPluginException;
27  import com.github.celldynamics.quimp.plugin.ecmm.ODEsolver;
28  import com.github.celldynamics.quimp.plugin.utils.IPadArray;
29  import com.github.celldynamics.quimp.plugin.utils.QuimpDataConverter;
30  
31  import ij.IJ;
32  import ij.process.ImageProcessor;
33  
34  /**
35   * Implementation of HatFilter for removing convexes from polygon.
36   * 
37   * <h3>List of user parameters:</h3>
38   * <ol>
39   * <li><i>window</i> - Size of window in pixels. It is responsible for sensitivity to protrusions of
40   * given size. Larger window can eliminate small and large protrusions whereas smaller window is
41   * sensitive only to small protrusions. Window size should be from 3 to number of outline points.
42   * <li><i>pnum</i> - Number of protrusions/cavities that will be removed from outline. If not
43   * limited by <i>alev</i> parameter the algorithm will eliminate <i>pnum</i> features
44   * (convex/concave parts) from outline. Feature is removed if its rank is bigger than <i>alev</i>
45   * threshold. If <i>pnum</i> is set to 0 algorithm will remove all candidates with rank higher than
46   * <i>alev</i> regardless their number. <i>pnum</i> should be from 0 to any value. Algorithm stops
47   * searching when there is no candidates to remove.
48   * <li><i>alev</i> - Threshold value, if circularity computed for given window position is lower
49   * than threshold this window is not eliminated regarding <i>pnum</i> or its rank in circularities.
50   * <i>alev</i> should be in range form 0 to ?, where 0 stands for accepting every candidate
51   * </ol>
52   * 
53   * <p><h3>General description of algorithm:</h3> The window slides over the wrapped contour. Points
54   * inside window for its position <i>p</i> are considered as candidates to removal from contour if
55   * they meet the following criterion:
56   * <ol>
57   * <li>The window has achieved for position p circularity parameter <i>c</i> larger than
58   * <i>alev</i>
59   * <li>The window on position <i>p</i> does not touch any other previously found window.
60   * <li>Points of window <i>p</i> are convex/concave (can be configured by {@link #setMode(int)}).
61   * </ol>
62   * 
63   * <p>Every window <i>p</i> has assigned a <i>rank</i>. Bigger <i>rank</i> stands for better
64   * candidate
65   * to remove. Algorithm tries to remove first <i>pnum</i> windows (those with biggest ranks) that
66   * meet above rules. If <i>pnum</i> is set to 0 all protrusions with rank > <i>alev</i> are deleted.
67   * 
68   * <p><H3>Detailed description of algorithm</H3> The algorithm comprises of three main steps:
69   * <ol>
70   * <li>Preparing <i>rank</i> table of candidates to remove
71   * <li>Iterating over <i>rank</i> table to find <i>pnum</i> such candidates who meet rules and store
72   * their coordinates in <i>ind2rem</i> array. By candidates it is understood sets of polygon indexes
73   * that is covered by window on given position. For simplification those vertexes are identified by
74   * lover and upper index of window in outline array (input). <i>pnum</i> can be 0, see note above.
75   * <li>Forming output table without protrusions.
76   * </ol>
77   * 
78   * <p><H2>First step</H2> The window of size <i>window</i> slides over looped data. Looping is
79   * performed by {@link Collections#rotate(List, int)} method that shift data left copying
80   * falling out indexes
81   * to end of the set (finally the window is settled in constant position between indexes
82   * <0;window-1>). For each its position <i>r</i> the candidate points are deleted from original
83   * contour and circularity is computed (see {@link #getCircularity(List)}). Then candidate points
84   * are passed to {@link #getWeighting(List)} method where weight is evaluated. The role of weight is
85   * to promote in <i>rank</i> candidate points that are cumulated in small area over distributed
86   * sets. Thus weight should give larger values for that latter distribution than for cumulated one.
87   * Currently weights are calculated as squared standard deviation of distances of all candidate
88   * points to
89   * center of mass of these points (or mean point if polygon is invalid). There is also mean
90   * intensity calculated by {@link #getIntensity(List, List, ImageProcessor)} (not obligatory, it can
91   * be switched off using {@link #runPlugin(List)} method.). It is sampled along
92   * shrunk outline produced from input points. Finally
93   * circularity(r) is divided by weight (<i>r</i>) and intensity and stored in <i>circ</i> array.
94   * Additionally in this step the convex/concave is checked. All candidate points are tested whether
95   * <b>all</b> they are inside the contour made without them (for concave option) or whether
96   * <b>any</b> of
97   * them is inside the modified contour (for convex option). Convex/concave sensitivity option can be
98   * modified by {@link #setMode(int)}
99   * This information is stored in <i>convex</i> array. Finally rank array <i>circ</i> is NOT
100  * normalised.
101  * 
102  * <p><H2>Second step</H2> In second step array of ranks <i>circ</i> is sorted in descending order.
103  * For every rank in sorted table the real position of window is retrieved (that gave this rank).
104  * The window position is defined here by two numbers - lover and upper range of indexes
105  * covered by it. The candidate points from this window are validated for criterion:
106  * <ol>
107  * <li><i>rank</i> must be greater than <i>alev</i>
108  * <li>lower and upper index of window (index means here number of polygon vertex in array) must not
109  * be included in any previously found window. This checking is done by deriving own class
110  * WindowIndRange with overwritten {@link HatSnakeFilter.WindowIndRange#compareTo(Object)} method
111  * that defines
112  * rules of equality and non relations between ranges. Basically any overlapping range or included
113  * is considered as equal and rejected from storing in <i>ind2rem</i> array.
114  * <li>candidate points must be convex or concave or any.
115  * </ol>
116  * If all above criterion are meet the window (l;u) is stored in <i>ind2rem</i>. Windows on the end
117  * of data are wrapped by dividing them for two sub-windows: (w;end) and (0;c) otherwise they may
118  * cover the whole range (e.g. <10;3> does not stand for window from 10 wrapped to 3 but window from
119  * 3 to 10).
120  * 
121  * <p>The second step is repeated until <i>pnum</i> object will be found or end of candidates will
122  * be reached. If <tt>pnum</tt> is 0 all objects that meet above criteria will be found.
123  * 
124  * <p><H2>Third step</H2> In third step every point from original contour is tested for including in
125  * array <i>ind2rem</i> that contains ranges of indexes to remove. Points on index that is not
126  * included in any of ranges stored in <i>ind2rem</i> are copied to output.
127  * 
128  * @author p.baniukiewicz
129  */
130 public class HatSnakeFilter implements IPadArray {
131 
132   static final Logger LOGGER = LoggerFactory.getLogger(HatSnakeFilter.class.getName());
133   /**
134    * Algorithm will look for cavities.
135    * 
136    * @see #setMode(int)
137    */
138   public static final int CAVITIES = 1;
139   /**
140    * Algorithm will look for protrusions.
141    * 
142    * @see #setMode(int)
143    */
144   public static final int PROTRUSIONS = 2;
145   /**
146    * Algorithm will look for cavities and protrusions.
147    * 
148    * @see #setMode(int)
149    */
150   public static final int ALL = 3;
151 
152   /**
153    * Number of steps used for outline shrinking for intensity sampling.
154    */
155   public int shrinkAmount = 3;
156 
157   /**
158    * Set it to 1 to look for inclusions, 2 for protrusions, 3 for all.
159    */
160   private int lookFor = PROTRUSIONS; // default for compatibility with old code
161 
162   private int window; // filter's window size
163   private int pnum; // how many protrusions to remove
164   private double alev; // minimal acceptance level
165   private int debugCounter = 0; // for naming of debug files
166 
167   /**
168    * Construct HatFilter Input array with data is virtually circularly padded.
169    */
170   public HatSnakeFilter() {
171     this.window = 15;
172     this.pnum = 1;
173     this.alev = 0;
174     LOGGER.debug("Set default parameter: window=" + window + " pnum=" + pnum + " alev=" + alev);
175   }
176 
177   /**
178    * Constructor passing algorithm parameters.
179    * 
180    * @param window size of the window
181    * @param pnum number of protrusions to find
182    * @param alev threshold
183    * @see HatSnakeFilter
184    */
185   public HatSnakeFilter(int window, int pnum, double alev) {
186     super();
187     this.window = window;
188     this.pnum = pnum;
189     this.alev = alev;
190   }
191 
192   /**
193    * In contrary to {@link #runPlugin(List, ImageProcessor, Pair)} this method does not use
194    * intensity weighting nor externally calculated ranks.
195    * 
196    * @param data contour to process
197    * @return Processed input list, size of output list may be different than input. Empty output
198    *         is also allowed.
199    * @throws QuimpPluginException on wrong input data
200    * @see #runPlugin(List, ImageProcessor, Pair)
201    * @see #runPlugin(List, ImageProcessor)
202    */
203   public List<Point2d> runPlugin(List<Point2d> data) throws QuimpPluginException {
204     return runPlugin(data, null);
205   }
206 
207   /**
208    * In contrary to {@link #runPlugin(List, ImageProcessor, Pair)} this method does not use
209    * externally calculated ranks.
210    * 
211    * @param data contour to process
212    * @param orgIp Original image used for sampling intensity, can be <tt>null</tt>
213    * @return Processed input list, size of output list may be different than input. Empty output
214    *         is also allowed.
215    * @throws QuimpPluginException on wrong input data
216    * @see #runPlugin(List)
217    * @see #runPlugin(List, ImageProcessor, Pair)
218    */
219   public List<Point2d> runPlugin(List<Point2d> data, ImageProcessor orgIp)
220           throws QuimpPluginException {
221     return runPlugin(data, orgIp, null);
222   }
223 
224   /**
225    * Main filter runner.
226    * 
227    * <p>This version assumes that user clicked Apply button to populate data from UI to plugin or
228    * any other ui element. User can expect that points will be always valid but they optionally may
229    * have 0 length.
230    * 
231    * @param points contour to process
232    * @param orgIp Original image used for sampling intensity, can be <tt>null</tt>
233    * @param ranks ranks calculated by {@link #calculateRank(List, ImageProcessor)} or <tt>null</tt>
234    *        to let them be evaluated by this method. Ranks values must comply with <tt>alev</tt>
235    *        value
236    * 
237    * @return Processed input list, size of output list may be different than input. Empty output
238    *         is also allowed.
239    * @throws QuimpPluginException on problem with input data
240    * @see #calculateRank(List, ImageProcessor)
241    */
242   public List<Point2d> runPlugin(List<Point2d> points, ImageProcessor orgIp,
243           Pair<ArrayList<Double>, ArrayList<Boolean>> ranks) throws QuimpPluginException {
244 
245     // internal parameters are not updated here but when user click apply
246     LOGGER.debug(String.format("Run plugin with params: window %d, pnum %d, alev %f", window, pnum,
247             alev));
248     // check input conditions
249     if (window % 2 == 0 || window < 0) {
250       throw new QuimpPluginException("Window must be uneven, positive and larger than 0");
251     }
252     if (window >= points.size()) {
253       throw new QuimpPluginException("Processing window to long");
254     }
255     if (window < 3) {
256       throw new QuimpPluginException("Window should be larger than 2");
257     }
258     if (pnum < 0) {
259       throw new QuimpPluginException("Number of protrusions should be larger than 0");
260     }
261     if (alev < 0) {
262       throw new QuimpPluginException("Acceptacne level should be positive");
263     }
264 
265     // Step 1 - Calculate ranks and normalise it - Give up - see tag:LocalAveraging
266     if (ranks == null) {
267       ranks = calculateRank(points, orgIp);
268     }
269 
270     ArrayList<Double> circ = ranks.getLeft(); // out ranks
271     ArrayList<Boolean> cavprot = ranks.getRight(); // shape flag
272 
273     // Step 2 - Check criterion for all windows
274     if (circ == null || circ.isEmpty()) {
275       LOGGER.info("No candidates found for selected mode: " + lookFor);
276       return points; // just return non-modified data;
277     }
278     TreeSet<WindowIndRange> ind2rem = new TreeSet<>(); // <l;u> range of indexes to remove
279     // need sorted but the old one as well to identify windows positions
280     // we do not touch circ itself but rater get list of indexes that refer to sorted element in
281     // original array. This is sort in ascending order
282     IntStream tmpStr = IntStream.range(0, circ.size()).boxed()
283             .sorted((i, j) -> circ.get(i).compareTo(circ.get(j))).mapToInt(el -> el);
284     List<Integer> sortedIndexes = tmpStr.boxed().collect(Collectors.toList());
285     Collections.reverse(sortedIndexes); // to have in descending orer - first index in this array
286     // point to index in sorted aray where largest element sits
287 
288     LOGGER.trace("cirI: " + sortedIndexes.toString());
289     LOGGER.trace("circ: " + circ.toString());
290     LOGGER.trace("circM: " + circ.get(sortedIndexes.get(0)).toString() + " (among ALL)");
291     for (int i = 0; i < sortedIndexes.size(); i++) { // for debug only
292       if (cavprot.get(sortedIndexes.get(i)) == true) {
293         LOGGER.trace(
294                 "circMacc: " + circ.get(sortedIndexes.get(i)).toString() + " (max requested type)");
295         break;
296       }
297     }
298 
299     if (circ.get(sortedIndexes.get(0)) < alev) {
300       LOGGER.info("Skipped this frame due to rank[0]=" + circ.get(sortedIndexes.get(0)));
301       return points; // just return non-modified data; TODO does it matter if circ is normalised?
302     }
303 
304     int found = 0; // how many protrusions we have found already
305     // current index in circsorted - number of window to analyze. Real position of
306     // window on data can be retrieved by finding value from sorted array circularity
307     // in non sorted, where order of data is related to window position starting
308     // from 0 for most left point of window
309     int i = 0; // position where window begins (linear index of sortedIndexes)
310     boolean contains; // temporary result of test if current window is included in any prev
311     // temporary variable for keeping window currently tested for containing in ind2rem
312     WindowIndRange indexTest = new WindowIndRange();
313 
314     // do as long as we can find candidates with rank higher than level and we found less than
315     // defined number of candidates. Latter criterion can be switched off by setting pnum to 0
316     // note that rank list contains all candidates and those with unwanted curvature type are
317     // filtered out in loop
318     while (i < sortedIndexes.size() && circ.get(sortedIndexes.get(i)) > alev
319             && (found < pnum || pnum == 0)) {
320       // find where it was before sorting and store in window positions
321       int startpos = sortedIndexes.get(i);
322       if (cavprot.get(startpos) == false || lookFor == ALL) { // not valid, skip unless ALL mode
323         i++;
324         continue;
325       }
326 
327       // check if we already have this index in list indexes to remove
328       if (startpos + window - 1 >= points.size()) { // if at end, we must turn to begin
329         indexTest.setRange(startpos, points.size() - 1); // to end
330         contains = ind2rem.contains(indexTest); // beginning of window at the end of dat
331         indexTest.setRange(0, window - (points.size() - startpos) - 1); // turn to start
332         contains &= ind2rem.contains(indexTest); // check rotated part at beginning
333       } else {
334         indexTest.setRange(startpos, startpos + window - 1);
335         contains = ind2rem.contains(indexTest);
336       }
337       if (contains == true) { // if contains go to next candidate
338         i++;
339         continue;
340       }
341       // if (cavprot.get(startpos) == true || lookFor == ALL) {
342       // store range of indexes that belongs to window
343       if (startpos + window - 1 >= points.size()) { // as prev split to two windows
344         // if we are on the end of data
345         ind2rem.add(new WindowIndRange(startpos, points.size() - 1));
346         // turn window to beginning
347         ind2rem.add(new WindowIndRange(0, window - (points.size() - startpos) - 1));
348       } else {
349         ind2rem.add(new WindowIndRange(startpos, startpos + window - 1));
350       }
351       LOGGER.info("added win for i=" + i + " startpos=" + startpos + " coord:"
352               + points.get(startpos).toString() + "alev=" + circ.get(sortedIndexes.get(i)));
353       found++;
354       // }
355       i++;
356     }
357     if (i >= sortedIndexes.size()) { // no more data to check, probably we have less prot. pnum
358       LOGGER.info("Can find next candidate. You can use smaller window");
359     }
360     if (found == 0) {
361       LOGGER.info("No acceptable candidates found, having desired rank AND being requested type");
362     }
363     LOGGER.trace("[indexRange;Point]: " + ind2rem.toString());
364     LOGGER.trace("Found :" + found + " accepted windows");
365 
366     // Step 3 - remove selected windows from input data
367     // array will be copied to new one skipping points to remove
368     List<Point2d> out = new ArrayList<Point2d>(); // output table for plotting temporary results
369     for (i = 0; i < points.size(); i++) {
370       // set upper and lower index to the same value - allows to test particular index for its
371       // presence in any defined range
372       indexTest.setSame(i);
373       if (!ind2rem.contains(indexTest)) { // check if any window position (l and u bound)
374         out.add(new Point2d(points.get(i)));
375       } else { // include tested point. Copy it to new array if not
376         LOGGER.trace("winpos: " + ind2rem.toString() + " " + points.get(i));
377       }
378     }
379     return out;
380   }
381 
382   /**
383    * Evaluate rank for given outline. Return raw unnormalised values.
384    * 
385    * <p>Rank is evaluated for each position of window (defined in
386    * {@link #HatSnakeFilter(int, int, double)} and it takes under account shape and local image
387    * intensity. If image is <tt>null</tt> only shape features are considered.
388    * 
389    * @param points Outline as list of points
390    * @param orgIp Image for sampling intensity along constricted outline. Can be null
391    * @return Ranks (left) and cavprot flag (right). Indexes in these arrays correlate with indexes
392    *         in input array. E.g. rank[i] stand for rank evaluated for window at position input[i]
393    *         (points input[i] - input[i+window-1]). Shape flag is always true for selected type
394    *         (CAVITIES or PROTRUSIONS). For ALL it is ignored.
395    */
396   public Pair<ArrayList<Double>, ArrayList<Boolean>> calculateRank(List<Point2d> points,
397           ImageProcessor orgIp) {
398     Path tmpDebug;
399     PrintWriter pw = null;
400     List<Point2d> shCont = new ArrayList<>();
401     // create shrunk outline to sample intensity - one of the parameters used for candidate rank
402     // this is unnecessary if there is no image provided but kept here for code simplicity
403     Outline outline = new QuimpDataConverter(points).getOutline(); // FIXME What if more contours?
404     // shrink original to sample intensities close cortex - used for detecting vesicles that are
405     // holes in cortex area.
406     outline.scaleOutline(shrinkAmount, -0.3, 0.1, 0.01);
407     outline.unfreezeAll();
408     outline.correctDensity(1, 0.5); // dense shape, shrank has different number of verts than org
409     shCont = outline.asList();
410 
411     if (QuimP.SUPER_DEBUG) {
412       tmpDebug = Paths.get(System.getProperty("java.io.tmpdir"),
413               "HatSnakeFilter_debug" + debugCounter + ".csv");
414       try {
415         pw = new PrintWriter(new FileWriter(tmpDebug.toFile()), true);
416       } catch (IOException e) {
417         e.printStackTrace();
418       }
419       Path shContDebug =
420               Paths.get(System.getProperty("java.io.tmpdir"), "shCont_debug" + debugCounter);
421       Path pointsDebug =
422               Paths.get(System.getProperty("java.io.tmpdir"), "points_debug" + debugCounter);
423       debugSaveList(shContDebug, shCont);
424       debugSaveList(pointsDebug, points);
425       debugCounter++;
426     }
427 
428     BasicPolygonsuimp/geom/BasicPolygons.html#BasicPolygons">BasicPolygons bp = new BasicPolygons(); // provides geometry processing
429     // Step 1 - Build circularity table
430     // array to store circularity for window positions. Index is related to window position
431     // (negative shift in rotate)
432     ArrayList<Double> circ = new ArrayList<Double>();
433     // store information if points for window at r position are convex compared to shape without
434     // these points
435     ArrayList<Boolean> cavprot = new ArrayList<Boolean>();
436 
437     double tmpCirc;
438     double tmpInt; // mean intensity along contour in window
439     double tmpWei; // weighting based on local shape disturbation
440     for (int r = 0; r < points.size(); r++) {
441       // get all points except window. Window has constant position 0 - (window-1)
442       List<Point2d> pointsnowindow = points.subList(window, points.size());
443       tmpCirc = getCircularity(pointsnowindow);
444       // calculate weighting for circularity
445       List<Point2d> pointswindow = points.subList(0, window); // get points for window only
446       // will return 1.0 if there is no image provided
447       tmpInt = getIntensity(shCont, pointswindow, orgIp); // mean intensity for window points
448       tmpInt = tmpInt == 0.0 ? 1.0 : tmpInt; // remove 0 as we divide weight later
449       tmpWei = getWeighting(pointswindow);
450       double rank = tmpCirc / (tmpWei * tmpInt); // calculate rank for window content
451       circ.add(rank); // store weighted circularity for shape without window
452       // check if points of window are convex according to shape without these points
453       boolean tmpCon;
454       switch (lookFor) {
455         case CAVITIES:
456           tmpCon = bp.areAllPointsInside(pointsnowindow, pointswindow); // true if concave
457           break;
458         case PROTRUSIONS:
459         case ALL:
460         default:
461           tmpCon = bp.areAllPointOutside(pointsnowindow, pointswindow); // true if protrusions
462       }
463       // true values are those that interest us (either concave or protrusions)
464       // for ALL yhis list is ignored
465       cavprot.add(tmpCon);
466       // move window to next position rotates by -1 what means that on first n positions
467       // of points there are different values simulate window
468       // first iter 0 1 2 3 4 5... (w=[0 1 2])
469       // second itr 1 2 3 4 5 0... (w=[1 2 3])
470       // last itera 5 0 1 2 3 4... (w=[5 0 1])
471       Collections.rotate(points, -1);
472       Collections.rotate(shCont, -1);
473       // dump to file
474       if (QuimP.SUPER_DEBUG) {
475         pw.print(r + ",");
476         pw.print(IJ.d2s(tmpCirc, 15) + ","); // circularity only
477         pw.print(IJ.d2s(tmpInt, 15) + ","); // intensity
478         pw.print(IJ.d2s(tmpWei, 15) + ","); // shape distribution weight
479         pw.print(IJ.d2s(rank, 15) + ","); // final rank unscaled
480         pw.print(tmpCon + ","); // boolean concavity
481         pw.print(lookFor + ","); // boolean type of algorithm used
482         pw.print(pointswindow); // coordinates of points in window
483         pw.println();
484       }
485     }
486     if (QuimP.SUPER_DEBUG) {
487       pw.close();
488     }
489     Pair<ArrayList<Double>, ArrayList<Boolean>> ret = new MutablePair<>(circ, cavprot);
490     return ret;
491   }
492 
493   /**
494    * Debug - saves list as tab separated coordinates.
495    * 
496    * @param name name and path
497    * @param list list to save
498    */
499   private void debugSaveList(Path name, List<Point2d> list) {
500     try {
501       PrintWriter pw = new PrintWriter(new FileWriter(name.toFile()), true);
502       int l = 0;
503       pw.print(list.size() + "\n");
504       for (Point2d p : list) {
505         pw.print(l + "\t" + IJ.d2s(p.getX(), 2) + "\t" + IJ.d2s(p.getY(), 2) + "\n");
506         l++;
507       }
508       pw.close();
509     } catch (IOException e) {
510       e.printStackTrace();
511     }
512   }
513 
514   /**
515    * Set detection mode.
516    * 
517    * @param mode can be CAVITIES, PROTRUSIONS, BOTH
518    */
519   public void setMode(int mode) {
520     lookFor = mode;
521   }
522 
523   /**
524    * Compute mean intensity for list of points. 3x3 stencil is used for each point.
525    * 
526    * <p>Shrank contour and original contour have different number of points so direct mapping is not
527    * possible. For each point in window the method finds closest point in shrank contour and sample
528    * 3x3 stencil around. All stencils for all window points are averaged then.
529    * 
530    * @param shpoints shrink contour used for sampling intensities
531    * @param pointswindow coordinates of window in original outline
532    * @param orgIp image
533    * @return 1.0 if input image is null. mean otherwise
534    */
535   double getIntensity(List<Point2d> shpoints, List<Point2d> pointswindow, ImageProcessor orgIp) {
536     double meanI = 0.0;
537     if (orgIp == null) {
538       return 1.0; // case where intensity is not used
539     }
540     for (Point2d p : pointswindow) {
541       Point2d closest = findClosest(shpoints, p);
542       meanI += ODEsolver.sampleFluo(orgIp, (int) Math.round(closest.getX()),
543               (int) Math.round(closest.getY()));
544     }
545     meanI /= pointswindow.size();
546     return meanI;
547   }
548 
549   /**
550    * Find point in list that is closest to given reference point.
551    * 
552    * @param shpoints List of points to search in
553    * @param p Reference point
554    * @return Point from list that is closes to reference point
555    */
556   private Point2d findClosest(List<Point2d> shpoints, Point2d p) {
557     double dist = Double.MAX_VALUE;
558     int minDistIndex = 0;
559     for (int i = 0; i < shpoints.size(); i++) {
560       Point2d loc = shpoints.get(i);
561       double d = Math.sqrt((loc.x - p.x) * (loc.x - p.x) + (loc.y - p.y) * (loc.y - p.y));
562       if (d < dist) {
563         dist = d;
564         minDistIndex = i;
565       }
566     }
567     return shpoints.get(minDistIndex);
568   }
569 
570   /**
571    * Calculate circularity of polygon.
572    * 
573    * <p>Circularity is computed as: \f[ circ=\frac{4*\pi*A}{P^2} \f] where \f$A\f$ is polygon area
574    * and \f$P\f$ is its perimeter
575    * 
576    * @param p Polygon vertices
577    * @return circularity
578    */
579   double getCircularity(final List<? extends Tuple2d> p) {
580     double area;
581     double perim;
582     BasicPolygonsquimp/geom/BasicPolygons.html#BasicPolygons">BasicPolygons b = new BasicPolygons();
583     area = b.getPolyArea(p);
584     perim = b.getPolyPerim(p);
585 
586     return (4 * Math.PI * area) / (perim * perim);
587   }
588 
589   /**
590    * Calculates weighting based on distribution of window points.
591    * 
592    * <p>Calculates center of mass of window points and then standard deviations of lengths between
593    * this point and every other point. Cumulated distributions like protrusions give smaller
594    * values than elongated ones.
595    * If input polygon <i>p</i> (which is only part of whole cell shape) is defective, i.e its
596    * edges cross, the weight is calculated using middle vector defined as mean of coordinates.
597    * 
598    * @param p Polygon vertices
599    * @return Weight
600    */
601   double getWeighting(final List<Point2d> p) {
602     double[] len = new double[p.size()];
603     BasicPolygonsuimp/geom/BasicPolygons.html#BasicPolygons">BasicPolygons bp = new BasicPolygons();
604     Vector2d middle;
605     try { // check if input polygon is correct
606       middle = new Vector2d(bp.polygonCenterOfMass(p));
607     } catch (IllegalArgumentException e) { // if not get middle point as mean
608       double mx = 0;
609       double my = 0;
610       for (Point2d v : p) {
611         mx += v.x;
612         my += v.y;
613       }
614       middle = new Vector2d(mx / p.size(), my / p.size());
615     }
616     int i = 0;
617     // get lengths
618     for (Point2d v : p) {
619       Vector2d vec = new Vector2d(middle); // vector between px and middle
620       vec.sub(v);
621       len[i++] = vec.length();
622     }
623     // get mean
624     double mean = 0;
625     for (double d : len) {
626       mean += d;
627     }
628     mean /= p.size();
629     // get std
630     double std = 0;
631     for (double d : len) {
632       std += Math.pow(d - mean, 2.0);
633     }
634     std /= p.size();
635     std = Math.sqrt(std);
636 
637     // max of len
638     // double maxLen = QuimPArrayUtils.arrayMax(len);
639     LOGGER.debug("getWeighting= " + std);
640     return std * std;
641   }
642 
643   /**
644    * Class holding lower and upper index of window. Supports comparisons.
645    * 
646    * <p>Two ranges (l;u) and (l1;u1) are equal if any of these conditions is met:
647    * <ol>
648    * <li>they overlap
649    * <li>they are the same
650    * <li>one is included in second
651    * </ol>
652    * 
653    * @author p.baniukiewicz
654    * @see WindowIndRange#compareTo(Object)
655    */
656   class WindowIndRange implements Comparable<Object> {
657     public int lower;
658     public int upper;
659 
660     public WindowIndRange() {
661       upper = 0;
662       lower = 0;
663     }
664 
665     /**
666      * Create pair of indexes that define window.
667      * 
668      * @param l lower index
669      * @param u upper index
670      */
671     WindowIndRange(int l, int u) {
672       setRange(l, u);
673     }
674 
675     @Override
676     public String toString() {
677       return "{" + lower + "," + upper + "}";
678     }
679 
680     public int hashCode() {
681       int result = 1;
682       result = 31 * result + lower;
683       result = 31 * result + upper;
684       return result;
685     }
686 
687     /**
688      * Compare two WindowIndRange objects.
689      * 
690      * @param obj object to compare
691      * @return true only if ranges does not overlap
692      */
693     @Override
694     public boolean equals(Object obj) {
695       if (this == obj) {
696         return true;
697       }
698       if (obj == null) {
699         return false;
700       }
701       if (getClass() != obj.getClass()) {
702         return false;
703       }
704 
705       final WindowIndRange other = (WindowIndRange) obj;
706       if (upper < other.lower) {
707         return true;
708       } else if (lower > other.upper) {
709         return true;
710       } else {
711         return false;
712       }
713 
714     }
715 
716     /**
717      * Compare two WindowIndRange objects.
718      * 
719      * <p>The following rules of comparison are used:
720      * <ol>
721      * <li>If range1 is below range2 they are not equal
722      * <li>If range1 is above range2 they are not equal
723      * </ol>
724      * 
725      * <p>They are equal in all other cases:
726      * <ol>
727      * <li>They are sticked
728      * <li>One includes other
729      * <li>They overlap
730      * </ol>
731      * 
732      * @param obj Object to compare to this
733      * @return -1,0,1 expressing relations in windows positions
734      */
735     @Override
736     public int compareTo(Object obj) {
737       final WindowIndRange other = (WindowIndRange) obj;
738       if (this == obj) {
739         return 0;
740       }
741 
742       if (upper < other.lower) {
743         return -1;
744       } else if (lower > other.upper) {
745         return 1;
746       } else {
747         return 0;
748       }
749     }
750 
751     /**
752      * Sets upper and lower indexes to the same value.
753      * 
754      * @param i Value to set for u and l
755      */
756     public void setSame(int i) {
757       lower = i;
758       upper = i;
759     }
760 
761     /**
762      * Set pair of indexes that define window assuring that l < u.
763      * 
764      * @param l lower index, always smaller
765      * @param u upper index
766      */
767     public void setRange(int l, int u) {
768       if (l > u) {
769         this.lower = u;
770         this.upper = l;
771       } else {
772         this.lower = l;
773         this.upper = u;
774       }
775     }
776 
777   }
778 }