View Javadoc
1   package com.github.celldynamics.quimp.plugin.randomwalk;
2   
3   import java.awt.Color;
4   import java.util.ArrayList;
5   import java.util.Collections;
6   import java.util.List;
7   import java.util.stream.Collectors;
8   import java.util.stream.IntStream;
9   
10  import org.slf4j.Logger;
11  import org.slf4j.LoggerFactory;
12  
13  import com.github.celldynamics.quimp.plugin.randomwalk.RandomWalkSegmentation.SeedTypes;
14  
15  import ij.ImagePlus;
16  import ij.ImageStack;
17  import ij.gui.Roi;
18  import ij.plugin.ZProjector;
19  import ij.process.ByteProcessor;
20  import ij.process.ColorProcessor;
21  import ij.process.ImageProcessor;
22  import ij.process.ImageStatistics;
23  
24  /**
25   * Contain various methods for converting labelled images to Seeds.
26   * 
27   * @author p.baniukiewicz
28   *
29   */
30  public class SeedProcessor {
31  
32    /**
33     * The Constant LOGGER.
34     */
35    static final Logger LOGGER = LoggerFactory.getLogger(SeedProcessor.class.getName());
36  
37    /**
38     * Decode RGB seed images to separate binary images. Support multiple foregrounds labels.
39     * 
40     * <p>Seeded RGB image is decomposed to separated binary images that contain only seeds.
41     * E.g. <b>FOREGROUND</b> image will have only pixels that were labelled as foreground.
42     * Decomposition is performed with respect to provided label colours.
43     * 
44     * @param rgb original image
45     * @param fseed foreground seed image
46     * @param bseed background seed image
47     * @return Seed structure with separated seeds
48     * @throws RandomWalkException on problems with decoding, unsupported image or empty list.
49     *         Exception is thrown only when all seed images for key are empty
50     * @see SeedProcessor#decodeSeedsfromRgb(ImagePlus, List, Color)
51     */
52    public static Seeds decodeSeedsfromRgb(final ImageProcessor rgb, final List<Color> fseed,
53            final Color bseed) throws RandomWalkException {
54      // output map integrating two lists of points
55      Seedsamics/quimp/plugin/randomwalk/Seeds.html#Seeds">Seeds out = new Seeds(2);
56      // output lists of points. Can be null if points not found
57      ImageProcessor background = new ByteProcessor(rgb.getWidth(), rgb.getHeight());
58      // verify input condition
59      if (rgb.getBitDepth() != 24) {
60        throw new RandomWalkException("Unsupported seed image type");
61      }
62      List<Color> fseeds = new ArrayList<>(fseed);
63      fseeds.add(bseed); // integrate in the same list
64      // find marked pixels
65      ColorProcessor cp = (ColorProcessor) rgb; // can cast here because of type checking
66      for (Color color : fseeds) { // iterate over all fg (and one bg) seeds
67        // foreground for current color
68        ImageProcessor foreground = new ByteProcessor(rgb.getWidth(), rgb.getHeight());
69        for (int x = 0; x < cp.getWidth(); x++) {
70          for (int y = 0; y < cp.getHeight(); y++) {
71            Color c = cp.getColor(x, y); // get color for pixel
72            if (c.equals(color)) { // if current pixel has "our" color
73              if (color.equals(bseed)) { // and it is from background seed
74                background.putPixel(x, y, 255); // add it to background map
75              } else { // otherwise store in foreground
76                foreground.putPixel(x, y, 255); // remember foreground coords
77              }
78            }
79          }
80        }
81        if (color.equals(bseed)) {
82          out.put(RandomWalkSegmentation.SeedTypes.BACKGROUND, background); // only one background
83        } else {
84          // many possible foregrounds
85          out.put(RandomWalkSegmentation.SeedTypes.FOREGROUNDS, foreground);
86        }
87      }
88      // check if there is at least one seed pixel in any seed map
89      validateSeeds(out, SeedTypes.FOREGROUNDS);
90      validateSeeds(out, SeedTypes.BACKGROUND);
91  
92      return out;
93    }
94  
95    /**
96     * Decode RGB seed images to separate binary images. Support multiple foregrounds labels.
97     * 
98     * <p>Seeded RGB image is decomposed to separated binary images that contain only seeds.
99     * E.g. <b>FOREGROUND</b> image will have only pixels that were labelled as foreground.
100    * Decomposition is performed with respect to provided label colours.
101    * 
102    * @param rgb RGB seed image
103    * @param fseed color of marker for foreground pixels
104    * @param bseed color of marker for background pixels
105    * @return Map containing extracted seed pixels from input RGB image that belong to foreground and
106    *         background. Map is addressed by two enums: <i>FOREGROUND</i> and <i>BACKGROUND</i>
107    * @throws RandomWalkException When image other that RGB provided
108    * @see #decodeSeedsfromRgb(ImageProcessor, List, Color)
109    */
110   public static Seeds decodeSeedsfromRgb(final ImagePlus rgb, final List<Color> fseed,
111           final Color bseed) throws RandomWalkException {
112     if (rgb.getType() != ImagePlus.COLOR_RGB) {
113       throw new RandomWalkException("Unsupported image type");
114     }
115     return decodeSeedsfromRgb(rgb.getProcessor(), fseed, bseed);
116   }
117 
118   /**
119    * Validate if at least one map in specified seed type contains non-zero pixel.
120    * 
121    * @param seeds seeds to verify
122    * @param type seed type
123    * @throws RandomWalkException when all maps under specified key are empty (black). Allows for
124    *         empty maps or nonexisting keys
125    */
126   public static void validateSeeds(Seeds seeds, SeedTypes type) throws RandomWalkException {
127     if (seeds.get(type) == null || seeds.get(type).isEmpty()) {
128       return;
129     }
130     // check if there is at least one seed pixel in any seed map
131     int pixelsNum = 0;
132     int pixelHistNum = 0;
133     // iterate over foregrounds
134     for (ImageProcessor i : seeds.get(type)) {
135       int[] histfg = i.getHistogram();
136       pixelHistNum += histfg[0]; // sum number of background pixels for each map
137       pixelsNum += i.getPixelCount(); // sum number of all pixels
138     }
139     if (pixelHistNum == pixelsNum) {
140       throw new RandomWalkException(
141               "Seed pixels are empty, check if:\n- correct colors were used\n- all slices have"
142                       + " been seeded (if stacked seed is used)\n"
143                       + "- Shrink/expand parameters are not too big.");
144     }
145   }
146 
147   /**
148    * Decode seeds from list of ROIs objects.
149    * 
150    * <p>ROIs naming must comply with the following pattern: coreID_NO, where core is different for
151    * FG and BG objects, ID is the id of object and NO is its number unique within ID (one object can
152    * ba labelled by several separated ROIs). All ROIs must have name set otherwise
153    * NullPointException is thrown.
154    * 
155    * @param rois list of ROIs with names
156    * @param fgName core for FG ROI name
157    * @param bgName core for BG core name
158    * @param width width of output map
159    * @param height height of output map
160    * @return Seed structure with FG and BG maps.
161    * @throws RandomWalkException when all seeds are empty (but maps exist)
162    */
163   public static Seeds decodeSeedsfromRoi(List<Roi> rois, String fgName, String bgName, int width,
164           int height) throws RandomWalkException {
165     Seedsamics/quimp/plugin/randomwalk/Seeds.html#Seeds">Seeds ret = new Seeds();
166 
167     List<Roi> fglist = rois.stream().filter(roi -> roi.getName().startsWith(fgName))
168             .collect(Collectors.toList());
169 
170     List<Roi> bglist = rois.stream().filter(roi -> roi.getName().startsWith(bgName))
171             .collect(Collectors.toList());
172 
173     // process backgrounds - all rois as one ID
174     ImageProcessor bg = new ByteProcessor(width, height);
175     bg.setColor(Color.WHITE);
176     for (Roi r : bglist) {
177       r.setFillColor(Color.WHITE);
178       r.setStrokeColor(Color.WHITE);
179       bg.draw(r);
180     }
181     if (!bglist.isEmpty()) {
182       ret.put(SeedTypes.BACKGROUND, bg);
183     }
184 
185     ArrayList<Integer> ind = new ArrayList<>(); // array of cell numbers
186     // init color for roi and collect unique cell id from name fgNameId_no
187     for (Roi r : fglist) {
188       r.setFillColor(Color.WHITE);
189       r.setStrokeColor(Color.WHITE);
190       String name = r.getName();
191       String i = name.substring(fgName.length(), name.length());
192       Integer n = Integer.parseInt(i.substring(0, i.indexOf("_")));
193       ind.add(n);
194     }
195     // remove duplicates from ids
196     List<Integer> norepeat = ind.stream().distinct().collect(Collectors.toList());
197     Collections.sort(norepeat); // unique ROIs
198     for (Integer i : norepeat) { // over unique
199       ImageProcessor fg = new ByteProcessor(width, height);
200       fg.setColor(Color.WHITE);
201       // find all with the same No within Id and add to image
202       fglist.stream().filter(roi -> roi.getName().startsWith(fgName + i))
203               .forEach(roi -> fg.draw(roi));
204       ret.put(SeedTypes.FOREGROUNDS, fg);
205     }
206 
207     // check if there is at least one seed pixel in any seed map
208     validateSeeds(ret, SeedTypes.FOREGROUNDS);
209     validateSeeds(ret, SeedTypes.BACKGROUND);
210     // LOGGER.debug(norepeat.toString());
211     // new ImagePlus("", ret.get(0).get(SeedTypes.BACKGROUND, 1)).show();
212     // List<ImageProcessor> tmp = ret.get(0).get(SeedTypes.FOREGROUNDS);
213     // for (ImageProcessor ip : tmp) {
214     // new ImagePlus("", ip).show();
215     // }
216     LOGGER.debug("Found " + norepeat.size() + " FG objects (" + fglist.size() + " total) and "
217             + bglist.size() + " BG seeds");
218     return ret;
219   }
220 
221   /**
222    * Convert the whole {@link Seeds} object into one-slice grayscale image. Background map has
223    * maximum intensity.
224    * 
225    * <p>If there is more than one BG map it is not possible to find which of last colours are they.
226    * 
227    * @param seeds seeds to convert
228    * @return Image with seeds in gray scale. Background is last (brightest). Null if there is no FG.
229    *         Empty BG is allowed.
230    * @see SeedProcessor#flatten(Seeds, SeedTypes, int)
231    */
232   public static ImageProcessor seedsToGrayscaleImage(Seeds seeds) {
233     if (seeds.get(SeedTypes.FOREGROUNDS) == null) {
234       return null;
235     }
236     ImageProcessor fg = flatten(seeds, SeedTypes.FOREGROUNDS, 1);
237     ImageStatistics stat = fg.getStats();
238     ImageProcessor bg = flatten(seeds, SeedTypes.BACKGROUND, (int) stat.max + 1);
239     ImageStack stack = new ImageStack(fg.getWidth(), fg.getHeight());
240     stack.addSlice(fg);
241     if (bg != null) {
242       stack.addSlice(bg);
243     }
244 
245     ImagePlus im = new ImagePlus("", stack);
246     ZProjector z = new ZProjector(im);
247     z.setImage(im);
248     z.setMethod(ZProjector.MAX_METHOD);
249     z.doProjection();
250     ImageProcessor ret = z.getProjection().getProcessor();
251     return ret;
252   }
253 
254   /**
255    * Flatten seeds of specified type and output grayscale image.
256    * 
257    * @param seeds seeds to flatten
258    * @param type which map
259    * @param initialValue brightness value to start from (typically 1)
260    * @return Image with seeds in gray scale or null if input does not contain specified map
261    * @see #seedsToGrayscaleImage(Seeds)
262    */
263   public static ImageProcessor flatten(Seeds seeds, SeedTypes type, int initialValue) {
264     if (seeds.get(type) == null) {
265       return null;
266     }
267     int currentVal = initialValue;
268     // assume same sizes of seeds
269     ImageStack stack = seeds.convertToStack(type).duplicate();
270     for (int s = 1; s <= stack.size(); s++) {
271       stack.getProcessor(s).multiply(1.0 * currentVal / 255);
272       currentVal++;
273     }
274     ImagePlus im = new ImagePlus("", stack);
275     ZProjector z = new ZProjector(im);
276     z.setImage(im);
277     z.setMethod(ZProjector.MAX_METHOD);
278     z.doProjection();
279     ImageProcessor ret = z.getProjection().getProcessor();
280     return ret;
281   }
282 
283   /**
284    * Convert grayscale image to {@link Seeds}.
285    * 
286    * <p>Pixels with the same intensity are collected in one map at {@link Seeds} structure under
287    * {@link SeedTypes#FOREGROUNDS} key. Works for separated objects as
288    * well. Collected maps are binary.
289    * 
290    * @param im 8-bit grayscale image, 0 is background
291    * @return Seeds with {@link SeedTypes#FOREGROUNDS} filled. No {@link SeedTypes#BACKGROUND}
292    * @throws RandomWalkException when all output FG seed maps are empty
293    */
294   public static Seeds decodeSeedsfromGrayscaleImage(ImageProcessor im) throws RandomWalkException {
295     Seedsamics/quimp/plugin/randomwalk/Seeds.html#Seeds">Seeds ret = new Seeds(2);
296     ImageStatistics stats = im.getStats();
297     int max = (int) stats.max; // max value
298     // list of all possible theoretical values of labels in image processor except background
299     // 1...max(im)
300     List<Integer> lin = IntStream.rangeClosed(1, max).boxed().collect(Collectors.toList());
301     ImageProcessor tmp = new ByteProcessor(im.getWidth(), im.getHeight());
302     // create requested number of foreground maps
303     for (int i = 0; i < max; i++) {
304       ret.put(SeedTypes.FOREGROUNDS, tmp.duplicate());
305     }
306     // fill each map with corresponding values
307     for (int r = 0; r < im.getHeight(); r++) {
308       for (int c = 0; c < im.getWidth(); c++) {
309         int pixel = (int) im.getPixelValue(r, c); // color of the pixel = slice in FG maps
310         if (pixel > 0) { // skip background
311           ret.get(SeedTypes.FOREGROUNDS).get(pixel - 1).set(r, c, Color.WHITE.getBlue());
312           // remove this map number from list - for further detection gaps in grayscale seeds that
313           // impose empty FG
314           lin.remove(new Integer(pixel));
315         }
316       }
317     }
318     // now lin should be empty, if not it means that there are gaps and some maps were not touched
319     if (!lin.isEmpty()) {
320       // remove starting from end
321       Collections.reverse(lin);
322       for (Integer i : lin) {
323         ret.get(SeedTypes.FOREGROUNDS).remove(i.intValue() - 1);
324       }
325     }
326     validateSeeds(ret, SeedTypes.FOREGROUNDS);
327     return ret;
328   }
329 
330   /**
331    * Convert list of ROIs to binary images separately for each ROI.
332    * 
333    * <p>Assumes that ROIs are named: fgNameID_NO, where ID belongs to the same object and NO are
334    * different scribbles for it. Similar to
335    * {@link #decodeSeedsfromRoi(List, String, String, int, int)}
336    * but process each slice separately.
337    *
338    * @param rois rois to process.
339    * @param fgName core for FG ROI name
340    * @param bgName core for BG core name
341    * @param width width of output map
342    * @param height height of output map
343    * @param slices number of slices
344    * @return List of Seeds for each slice
345    * @throws RandomWalkException when all seeds are empty (but maps exist)
346    * @see #decodeSeedsfromRoi(List, String, String, int, int)
347    */
348   public static List<Seeds> decodeSeedsfromRoiStack(List<Roi> rois, String fgName, String bgName,
349           int width, int height, int slices) throws RandomWalkException {
350     ArrayList<Seeds> ret = new ArrayList<>();
351     // find nonassigned ROIs - according to DOC getPosition() can return 0 as well (stacks start
352     // from 1)
353     List<Roi> col0 =
354             rois.stream().filter(roi -> roi.getPosition() == 0).collect(Collectors.toList());
355     // find ROIS on each slice
356     for (int s = 1; s <= slices; s++) {
357       final int w = s;
358       List<Roi> col =
359               rois.stream().filter(roi -> roi.getPosition() == w).collect(Collectors.toList());
360       // merge those nonassigned to slice 1
361       if (s == 1) {
362         col.addAll(col0);
363       }
364       // produce Seeds
365       Seeds tmpSeed = SeedProcessor.decodeSeedsfromRoi(col, fgName, bgName, width, height);
366       ret.add(tmpSeed);
367     }
368 
369     // new ImagePlus("", ret.get(0).get(SeedTypes.BACKGROUND, 1)).show();
370     // List<ImageProcessor> tmp = ret.get(0).get(SeedTypes.FOREGROUNDS);
371     // for (ImageProcessor ip : tmp) {
372     // new ImagePlus("", ip).show();
373     // }
374 
375     return ret;
376 
377   }
378 }