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 }