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 }