1 package com.github.celldynamics.quimp.geom; 2 3 import java.awt.Color; 4 import java.util.ArrayList; 5 import java.util.Iterator; 6 import java.util.List; 7 8 import org.apache.commons.lang3.tuple.ImmutablePair; 9 import org.apache.commons.lang3.tuple.Pair; 10 import org.scijava.vecmath.Point2d; 11 import org.slf4j.Logger; 12 import org.slf4j.LoggerFactory; 13 14 import com.github.celldynamics.quimp.Outline; 15 import com.github.celldynamics.quimp.plugin.utils.QuimpDataConverter; 16 17 import ij.ImagePlus; 18 import ij.gui.PolygonRoi; 19 import ij.gui.Roi; 20 import ij.gui.Wand; 21 import ij.process.ImageProcessor; 22 23 /* 24 * //!> 25 * @startuml doc-files/TrackOutline_1_UML.png 26 * User->(Create object) 27 * User->(Convert Outlines to Point2d) 28 * User->(get deep copy of Outlines) 29 * @enduml 30 * 31 * @startuml doc-files/TrackOutline_2_UML.png 32 * actor User 33 * User-->TrackOutline : <<create>>\n""image"",""frame"" 34 * TrackOutline->prepare : ""image"" 35 * note left 36 * Filtering and BW 37 * operations 38 * endnote 39 * prepare -> prepare : //open// 40 * prepare->prepare : //close// 41 * prepare->TrackOutline : ""prepared"" 42 * TrackOutline -> getOutlines 43 * loop every pixel 44 * getOutlines->getOutline : not background pixel [x,y] 45 * getOutline->Wand : [x,y] 46 * Wand->getOutline : ""xpoints"",""ypoints"" 47 * getOutline->SegmentedShapeRoi : <<create>> 48 * SegmentedShapeRoi-->getOutline 49 * getOutline->getOutline : clear ROI on image 50 * getOutline->SegmentedShapeRoi : set ""frame"" 51 * SegmentedShapeRoi-->getOutline 52 * getOutline->getOutlines : ""SegmentedShapeRoi"" 53 * getOutlines->getOutlines : store ""SegmentedShapeRoi"" 54 * getOutlines->getOutlines : store ""Color"" 55 * end 56 * getOutlines->TrackOutline 57 * @enduml 58 * 59 * //!< 60 */ 61 /** 62 * Convert grayscale masks into list of vertices in correct order. Stand as ROI holder. 63 * 64 * <p>The algorithm uses IJ tools for tracking and filling (deleting) objects It goes through all 65 * points of the image and for every visited point it checks whether the value is different than 66 * defined background. If it is, the Wand tool is used to select object given by the pixel value 67 * inside it. The ROI (outline) is then stored in this object and served as reference The ROI is 68 * then used to delete selected object from image (using background value). Next, the algorithm 69 * moves to next pixel (of the same image the object has been deleted from, so it is not possible to 70 * detect the same object twice). 71 * 72 * <p>It assigns also frame number to outline<br> 73 * <img src="doc-files/TrackOutline_1_UML.png"/><br> 74 * Creating object runs also outline detection and tracking. Detected outlines are stored in object 75 * and can be accessed by reference directly from outlines array or as copies from 76 * getCopyofShapes().<br> 77 * <img src="doc-files/TrackOutline_2_UML.png"/><br> 78 * 79 * @author p.baniukiewicz 80 * @see com.github.celldynamics.quimp.geom.SegmentedShapeRoi 81 * 82 */ 83 public class TrackOutline { 84 85 /** 86 * The Constant LOGGER. 87 */ 88 static final Logger LOGGER = LoggerFactory.getLogger(TrackOutline.class.getName()); 89 /** 90 * ROIs below this size (width or height) will be removed. 91 * 92 * @see #getOutlines(double, boolean) 93 * @see #getOutlinesasPoints(double, boolean) 94 * @see #getOutlinesColors(double, boolean) 95 * @see #getOutlineasRawPoints() 96 */ 97 static final int SIZE_LIMIT = 10; 98 /** 99 * Original image. It is not modified. 100 */ 101 protected ImageProcessor imp; 102 /** 103 * Image under process. It is modified by Outline methods. 104 */ 105 private ImageProcessor prepared; 106 107 /** 108 * List of found outlines as ROIs. 109 */ 110 public ArrayList<SegmentedShapeRoi> outlines; 111 112 /** 113 * List of colors of objects that were used to produce SegmentedShapeRoi. 114 * 115 * <p>This list is related to {@link TrackOutline#outlines}. Colors are encoded as rgb 116 * {@link Color#Color(int)} 117 */ 118 public ArrayList<Color> colors; 119 120 /** 121 * The background color. 122 */ 123 protected int background; 124 /** 125 * Maximal number of searched objects, all objects if negative. 126 */ 127 private int maxNumObj = -1; 128 /** 129 * Frame for which imp has been got. 130 */ 131 private int frame; 132 133 /** 134 * Constructor from ImageProcessor. 135 * 136 * @param imp Image to process (not modified) 137 * @param background Color value for background 138 * @param frame Frame of stack that \a imp belongs to 139 * @throws IllegalArgumentException when wrong image format is provided 140 */ 141 public TrackOutline(ImageProcessor imp, int background, int frame) { 142 if (imp.getBitDepth() != 8 && imp.getBitDepth() != 16) { 143 throw new IllegalArgumentException("Only 8-bit or 16-bit images are supported"); 144 } 145 outlines = new ArrayList<>(); 146 colors = new ArrayList<>(); 147 this.imp = imp; 148 this.background = background; 149 this.prepared = prepare(); 150 this.frame = frame; 151 getOutlines(); 152 } 153 154 /** 155 * Constructor from ImageProcessor for single images. 156 * 157 * @param imp Image to process (not modified) 158 * @param background Color value for background 159 * @throws IllegalArgumentException when wrong image format is provided 160 */ 161 public TrackOutline(ImageProcessor imp, int background) { 162 this(imp, background, 1); 163 } 164 165 /** 166 * Default constructor. 167 * 168 * @param im Image to process (not modified), 8-bit, one slice 169 * @param background Background color 170 */ 171 public TrackOutline(ImagePlus im, int background) { 172 this(im.getProcessor(), background, 1); 173 } 174 175 /** 176 * Filter input image to remove single pixels. 177 * 178 * <p>Implement closing followed by opening. 179 * 180 * <P>TODO If input image is grayscale this method may not work as expected, e.g. small pixels 181 * with one color sticked to larger objects with another color will not be removed 182 * 183 * @return Filtered processor 184 */ 185 public ImageProcessor prepare() { 186 ImageProcessor filtered = imp.duplicate(); 187 // closing 188 filtered.dilate(); 189 filtered.erode(); 190 // opening 191 filtered.erode(); 192 filtered.dilate(); 193 194 return filtered; 195 } 196 197 /** 198 * Get outline using Wand tool. 199 * 200 * @param col Any point inside region 201 * @param row Any point inside region 202 * @param color Color of object 203 * @return ShapeRoi that contains ROI for given object with assigned frame to it 204 * @throws IllegalArgumentException when wand was not able to find point 205 */ 206 SegmentedShapeRoi getOutline(int col, int row, int color) { 207 Wand wand = new Wand(prepared); 208 wand.autoOutline(col, row, color, color, Wand.EIGHT_CONNECTED); 209 if (wand.npoints == 0) { 210 throw new IllegalArgumentException("Wand: Points not found"); 211 } 212 Roi roi = new PolygonRoi(wand.xpoints, wand.ypoints, wand.npoints, Roi.FREEROI); 213 clearRoi(roi, background); 214 SegmentedShapeRoim/SegmentedShapeRoi.html#SegmentedShapeRoi">SegmentedShapeRoi ret = new SegmentedShapeRoi(roi); // create segmentation object 215 ret.setFrame(frame); // set current frame to this object 216 return ret; 217 } 218 219 /** 220 * Try to find all outlines on image. 221 * 222 * <p>It is possible to limit number of searched outlines setting maxNumObj > 0 The algorithm goes 223 * through every pixel on image and if this pixel is different than background (defined in 224 * constructor) it uses it as source of Wand. Wand should outline found object, which is then 225 * erased from image. then next pixel is analyzed. 226 * 227 * <p>Fills outlines field that contains list of all ROIs obtained for this image together with 228 * frame number assigned to TrackOutline. 229 * 230 * <p>Skip very small object. 231 * 232 * @see #SIZE_LIMIT 233 * 234 */ 235 private void getOutlines() { 236 // go through the image and look for non background pixels 237 outer: for (int r = 0; r < prepared.getHeight(); r++) { 238 for (int c = 0; c < prepared.getWidth(); c++) { 239 int pixel = prepared.getPixel(c, r); 240 if (pixel != background) { // non background pixel 241 // remember outline and delete it from input image 242 SegmentedShapeRoi sr = getOutline(c, r, pixel); 243 if (sr.getFloatWidth() < SIZE_LIMIT || sr.getFloatHeight() < SIZE_LIMIT) { 244 continue; 245 } 246 outlines.add(sr); 247 colors.add(new Color(pixel)); // store source color as rgb 248 if (maxNumObj > -1) { 249 if (outlines.size() >= maxNumObj) { 250 LOGGER.warn("Reached maximal number of outlines"); 251 break outer; 252 } 253 } 254 } 255 } 256 } 257 } 258 259 /** 260 * Convert found outlines to Outline. 261 * 262 * @param step resolution step 263 * @param smooth true to use IJ polygon smoothing (running average). 264 * @return List of Outline object that represents all 265 * @see SegmentedShapeRoi#getOutlineasPoints() 266 * @see #getOutlinesasPoints(double, boolean) 267 * @see #getOutlinesColors(double, boolean) 268 */ 269 public List<Outline> getOutlines(double step, boolean smooth) { 270 Pair<List<Outline>, List<Color>> ret = getOutlinesColors(step, smooth); 271 return ret.getLeft(); 272 } 273 274 /** 275 * Convert found outlines to Outline. 276 * 277 * @param step resolution step 278 * @param smooth true to use IJ polygon smoothing (running average). 279 * @return List of Outline object and colors of foreground pixels used to produce them coded as 280 * rgb by {@link Color#Color(int)} 281 * @see SegmentedShapeRoi#getOutlineasPoints() 282 * @see #getOutlinesasPoints(double, boolean) 283 */ 284 public Pair<List<Outline>, List<Color>> getOutlinesColors(double step, boolean smooth) { 285 List<SegmentedShapeRoi> rois = getCopyofShapes(); 286 // convert to Outlines from ROIs 287 ArrayList<Outline> outlines = new ArrayList<>(); 288 for (SegmentedShapeRoi sr : rois) { 289 // interpolate and reduce number of points 290 sr.setInterpolationParameters(step, false, smooth); 291 Outline o; 292 o = new QuimpDataConverter(sr.getOutlineasPoints()).getOutline(); 293 outlines.add(o); 294 } 295 296 return new ImmutablePair<List<Outline>, List<Color>>(outlines, colors); 297 } 298 299 /** 300 * Reformat collected outlines and Colors to list of pairs. 301 * 302 * @param step resolution step 303 * @param smooth true to use IJ polygon smoothing (running average). 304 * @return List of pairs, outlines and colors of pixels they were created from 305 */ 306 public List<Pair<Outline, Color>> getPairs(double step, boolean smooth) { 307 List<Outline> out = getOutlinesColors(step, smooth).getLeft(); 308 List<Pair<Outline, Color>> ret = new ArrayList<>(); 309 Iterator<Outline> ito = out.iterator(); 310 Iterator<Color> itc = colors.iterator(); 311 while (ito.hasNext() && itc.hasNext()) { 312 Pair<Outline, Color> p = new ImmutablePair<Outline, Color>(ito.next(), itc.next()); 313 ret.add(p); 314 } 315 return ret; 316 } 317 318 /** 319 * Erase roi on image stored in object with color bckColor. 320 * 321 * @param roi roi on this image 322 * @param bckColor color for erasing 323 */ 324 private void clearRoi(Roi roi, int bckColor) { 325 prepared.setColor(bckColor); 326 prepared.fill(roi); 327 } 328 329 /** 330 * Convert found outlines to List. 331 * 332 * @param step step - step during conversion outline to points. For 1 every point from outline 333 * is included in output list 334 * @param smooth true for using running average during interpolation 335 * @return List of List of ROIs 336 * @see SegmentedShapeRoi#getOutlineasPoints() 337 */ 338 public List<List<Point2d>> getOutlinesasPoints(double step, boolean smooth) { 339 List<List<Point2d>> ret = new ArrayList<>(); 340 for (SegmentedShapeRoi sr : outlines) { 341 sr.setInterpolationParameters(step, false, smooth); 342 ret.add(sr.getOutlineasPoints()); 343 } 344 return ret; 345 } 346 347 /** 348 * Convert found outlines to List without any interpolation. 349 * 350 * @return List of List of ROIs 351 * 352 * @see SegmentedShapeRoi#getOutlineasPoints() 353 */ 354 public List<List<Point2d>> getOutlineasRawPoints() { 355 List<List<Point2d>> ret = new ArrayList<>(); 356 for (SegmentedShapeRoi sr : outlines) { 357 ret.add(sr.getOutlineasRawPoints()); 358 } 359 return ret; 360 } 361 362 /** 363 * Return deep copy of Shapes. 364 * 365 * @return deep copy of Rois. 366 */ 367 public List<SegmentedShapeRoi> getCopyofShapes() { 368 ArrayList<SegmentedShapeRoi> clon = new ArrayList<>(); 369 for (SegmentedShapeRoi sr : outlines) { 370 clon.add((SegmentedShapeRoi) sr.clone()); 371 } 372 return clon; 373 } 374 375 /** 376 * Return shallow copy of Shapes. 377 * 378 * @return Rois found on image. 379 * 380 * @see #getColors() 381 */ 382 public List<SegmentedShapeRoi> getShapes() { 383 return outlines; 384 } 385 386 /** 387 * Get colors of pixels that outlines were produced from. 388 * 389 * <p>Size of this array and order of elements correspond to {@link #getShapes()} and all 390 * get methods in this class. 391 * 392 * @return the colors as RGB, created by {@link Color#Color(int)}. Integer can be retrieved by 393 * summing up three RGB components. 394 * 395 * @see #getShapes() 396 */ 397 public ArrayList<Color> getColors() { 398 return colors; 399 } 400 401 /** 402 * Set {@link Roi#setStrokeColor(Color)} of each found Roi to color of pixels it was produced 403 * from. 404 * 405 * <p>Modified are {@link SegmentedShapeRoi} stored in {@link #outlines} 406 */ 407 public void setColors() { 408 Iterator<SegmentedShapeRoi> ito = outlines.iterator(); 409 Iterator<Color> itc = colors.iterator(); 410 while (ito.hasNext() && itc.hasNext()) { 411 ito.next().setStrokeColor(itc.next()); 412 } 413 } 414 415 /* 416 * (non-Javadoc) 417 * 418 * @see java.lang.Object#toString() 419 */ 420 @Override 421 public String toString() { 422 return "\nTrackOutline [outlines=" + outlines + "]"; 423 } 424 425 }