View Javadoc
1   package com.github.celldynamics.quimp.plugin.binaryseg;
2   
3   import java.util.ArrayList;
4   
5   import org.slf4j.Logger;
6   import org.slf4j.LoggerFactory;
7   
8   import com.github.celldynamics.quimp.geom.SegmentedShapeRoi;
9   import com.github.celldynamics.quimp.geom.TrackOutline;
10  import com.github.celldynamics.quimp.plugin.QuimpPluginException;
11  
12  import ij.ImagePlus;
13  import ij.ImageStack;
14  import ij.gui.ShapeRoi;
15  
16  /*
17   * //!>
18   * @startuml doc-files/BinarySegmentation_1_UML.png
19   * User-->(Create BinarySegmentation)
20   * User->(run tracking)
21   * User->(get chains)
22   * (Create BinarySegmentation).->(create TrackOutline) : <<extend>>
23   * @enduml
24   * 
25   * @startuml doc-files/BinarySegmentation_2_UML.png
26   * actor User
27   * User->BinarySegmentation : <<create>>\n""image""
28   * loop for every frame
29   * BinarySegmentation->TrackOutline : <<create>>\n""slice"",""frame""
30   * activate TrackOutline
31   * TrackOutline-->BinarySegmentation : //obj//
32   * BinarySegmentation->BinarySegmentation : store //obj// in ""trackers""
33   * note left
34   * See TrackOutline
35   * trackers are ROIs for
36   * one slice kept in TrackOutline
37   * object
38   * end note
39   * end
40   * User->trackObjects
41   * loop for every tracker //o2//
42   * loop for every object in tracker //sR//
43   * trackObjects->TrackOutline : get ""outlines""
44   * TrackOutline->trackObjects : ""outlines""
45   * note left : references
46   * trackObjects->testIntersect : ""sR"",""o2""
47   * testIntersect->trackObjects : set ID to current outline
48   * note right
49   * Modify reference in TrackOutline
50   * end note
51   * testIntersect->trackObjects : set parent ID to next outline
52   * note left
53   * Test for current object and all
54   * on next frame
55   * end note
56   * end
57   * end
58   * User->getChains
59   * getChains->getChains : sort according to ID
60   * getChains->User : return array
61   * @enduml
62   * 
63   * //!<
64   */
65  /**
66   * Run Binary segmentation converting grayscale masks to ordered ROIs.
67   * 
68   * <p>This class mainly join subsequent outlines to chains that contain outlines related by origin (
69   * when next outline originates from previous - it means that next object overlap previous one) The
70   * segmentation itself - generation of outlines for one slice is done in TrackOutline class
71   * 
72   * <p>The ROIs are grouped according to their origin and they have assigned frame number where they
73   * appeared. The algorithm is as follows: The frames from input stack from first to before last are
74   * processed. For every i-th frame the outlines are obtained and compared with i+1 frame. If any of
75   * k-th outline from i+1 frame overlap l-th outline on i-th frame, the k-th outline gets the same id
76   * as l-th but only if k-th does not have any ID yet. There for if there is outline that does not
77   * have source on i-th frame, it will skipped now but it will be found in next iteration and because
78   * it does not have ID, the new will be assigned to it.
79   * 
80   * <p>If there is break in chain (missing object), the object on the next frame will begin the new
81   * chain. <br>
82   * <img src="doc-files/BinarySegmentation_1_UML.png"/><br>
83   * After creation of object user has to call trackObjects() to run tracking. Segmentation is run on
84   * object creation. Finally, getChains() should be called to get results - chains of outlines. <br>
85   * <img src="doc-files/BinarySegmentation_2_UML.png"/><br>
86   * 
87   * <p>For grayscale input algorithm compares stroke color of ROIs that can be set to color of
88   * pixels, the roi was taken from by {@link TrackOutline#setColors()}. This assume that color does
89   * not change for particular cell within frame.
90   * 
91   * @author p.baniukiewicz
92   * @see TrackOutline
93   *
94   */
95  public class BinarySegmentation {
96  
97    /**
98     * The Constant LOGGER.
99     */
100   static final Logger LOGGER = LoggerFactory.getLogger(BinarySegmentation.class.getName());
101 
102   private int nextID = 0; // next free ID
103   private ImagePlus ip; // image to process (stack)
104   /**
105    * Predefined background color.
106    */
107   int backgroundColor = 0;
108   /**
109    * Array of segmented slices. One TrackOutline object can have some outlines, depending how many
110    * objects were on this slice
111    */
112   private TrackOutline[] trackers;
113   /**
114    * True if all slices are 2 color.
115    * 
116    * <p>This trigger different method of correlation objects between frames. For binary slices (all)
117    * overlapping is tested, for grayscale we use color of pixels the Rois was taken from, copied by
118    * {@link TrackOutline#setColors()}
119    */
120   private boolean isBinary = true;
121 
122   /**
123    * Constructor for segmentation of stack.
124    * 
125    * @param ip stack of images to segment
126    * @throws QuimpPluginException when image is null or wrong type
127    */
128   public BinarySegmentation(final ImagePlus ip) throws QuimpPluginException {
129     if (ip == null) {
130       throw new QuimpPluginException("The image was null");
131     }
132     if (!ip.getProcessor().isGrayscale()) {
133       throw new QuimpPluginException("Input image must be 8-bit");
134     }
135 
136     this.ip = ip.duplicate();
137     // determine method of corelating between frames
138     for (int s = 1; s <= ip.getImageStackSize(); s++) {
139       isBinary = isBinary && ip.getStack().getProcessor(s).isBinary();
140     }
141     LOGGER.debug("Got " + ip.getImageStackSize() + " slices");
142     trackers = new TrackOutline[this.ip.getImageStackSize()];
143     ImageStack ips = this.ip.getStack();
144     for (int i = 0; i < trackers.length; i++) {
145       trackers[i] = new TrackOutline(ips.getProcessor(i + 1), backgroundColor, i + 1); // outlining
146       // set stroke color for ROI, assume that after segmentation the same cells will have the same
147       // color
148       if (isBinary == false) {
149         trackers[i].setColors();
150       }
151     }
152   }
153 
154   /**
155    * Test whether two ROIs overlap or have the same color. Modify r1 parameter
156    * 
157    * @param r1 First ROI - it will be modified!
158    * @param r2 Second ROI
159    * @return true if r1 and r2 overlap
160    */
161   private boolean testIntersect(final ShapeRoi r1, final ShapeRoi r2) {
162     if (r1 == null || r2 == null) {
163       return false;
164     }
165     if (isBinary == true) {
166       ShapeRoi intersect = r1.and(r2);
167       if (intersect.getFloatWidth() == 0 || intersect.getFloatHeight() == 0) {
168         LOGGER.debug(r1 + " and " + r2 + " do not intersect");
169         return false;
170       } else {
171         LOGGER.debug(r1 + " and " + r2 + " do intersect");
172         return true;
173       }
174     } else { // not binary image on input, use color codes
175       if (r1.getStrokeColor().equals(r2.getStrokeColor())) {
176         return true;
177       } else {
178         return false;
179       }
180     }
181   }
182 
183   /**
184    * Test whether given ROI overlap any of ROI in array and assign correct ID to ROIs
185    * 
186    * <p>If any of sRa overlap sR, the roi from array gets the same ID as \a sR. If \a sR does
187    * not have ID it get the new one
188    * 
189    * @param sr ROI to test (not modified)
190    * @param sra Array of ROIs to test
191    * 
192    */
193   private void testIntersect(final SegmentedShapeRoi sr, final ArrayList<SegmentedShapeRoi> sra) {
194     if (sr.getId() == SegmentedShapeRoi.NOT_COUNTED) { // root - first outline
195       sr.setId(nextID++); // if not counted start new chain assigning new id
196     }
197     for (SegmentedShapeRoi s : sra) {
198       if (testIntersect((ShapeRoi) sr.clone(), s) == true) {
199         s.setId(sr.getId()); // next outline has the same id
200         break; // do not look more on this set (this frame)
201       }
202     }
203   }
204 
205   /**
206    * Main runner for tracking.
207    * 
208    * <p>In result of this method the ROIs kept in TrackOutline objects will be modified by giving
209    * them IDs of their parent.
210    */
211   public void trackObjects() {
212     if (trackers.length == 1) { // only one slice, use the same reference for testIntersect
213       ArrayList<SegmentedShapeRoi> o1 = trackers[0].outlines; // get frame current
214       ArrayList<SegmentedShapeRoi> o2 = trackers[0].outlines; // and next
215       for (SegmentedShapeRoi sr : o1) { // iterate over all objects in current frame
216         testIntersect(sr, o2); // and find its child if any on next frame
217       }
218     } // loop below does not fire for one slice
219     for (int f = 0; f < trackers.length - 1; f++) { // iterate over frames
220       ArrayList<SegmentedShapeRoi> o1 = trackers[f].outlines; // get frame current
221       ArrayList<SegmentedShapeRoi> o2 = trackers[f + 1].outlines; // and next
222       for (SegmentedShapeRoi sr : o1) { // iterate over all objects in current frame
223         testIntersect(sr, o2); // and find its child if any on next frame
224       }
225     }
226     // check if we have any uncounted object at last frame. It can happen if there is lonely object
227     // that exists only at last frame
228     ArrayList<SegmentedShapeRoi> o2 = trackers[trackers.length - 1].outlines;
229     for (SegmentedShapeRoi sr : o2) {
230       if (sr.getId() == SegmentedShapeRoi.NOT_COUNTED) {
231         sr.setId(nextID++);
232       }
233     }
234   }
235 
236   /**
237    * Compose chains of object related to each others along frames.
238    * 
239    * <p>Relation means that previous object and next one overlap, thus their segmentations will be
240    * assigned to the same group and they will be in correct order as they appeared in stack
241    * 
242    * @return List of Lists that contains outlines. First level of list is the chain (found related
243    *         objects), the second level are outlines for this chain. Every outline has coded frame
244    *         where it appeared.
245    */
246   public ArrayList<ArrayList<SegmentedShapeRoi>> getChains() {
247     ArrayList<ArrayList<SegmentedShapeRoi>> ret = new ArrayList<>(nextID);
248     for (int i = 0; i < nextID; i++) {
249       ret.add(new ArrayList<>());
250     }
251     for (TrackOutline to : trackers) { // go through all Outlines and sort them for ID
252       for (SegmentedShapeRoi ss : to.outlines) {
253         ret.get(ss.getId()).add(ss);
254       }
255     }
256     return ret;
257   }
258 }