View Javadoc
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 }