SeedProcessor.java

package com.github.celldynamics.quimp.plugin.randomwalk;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.celldynamics.quimp.plugin.randomwalk.RandomWalkSegmentation.SeedTypes;

import ij.ImagePlus;
import ij.ImageStack;
import ij.gui.Roi;
import ij.plugin.ZProjector;
import ij.process.ByteProcessor;
import ij.process.ColorProcessor;
import ij.process.ImageProcessor;
import ij.process.ImageStatistics;

/**
 * Contain various methods for converting labelled images to Seeds.
 * 
 * @author p.baniukiewicz
 *
 */
public class SeedProcessor {

  /**
   * The Constant LOGGER.
   */
  static final Logger LOGGER = LoggerFactory.getLogger(SeedProcessor.class.getName());

  /**
   * Decode RGB seed images to separate binary images. Support multiple foregrounds labels.
   * 
   * <p>Seeded RGB image is decomposed to separated binary images that contain only seeds.
   * E.g. <b>FOREGROUND</b> image will have only pixels that were labelled as foreground.
   * Decomposition is performed with respect to provided label colours.
   * 
   * @param rgb original image
   * @param fseed foreground seed image
   * @param bseed background seed image
   * @return Seed structure with separated seeds
   * @throws RandomWalkException on problems with decoding, unsupported image or empty list.
   *         Exception is thrown only when all seed images for key are empty
   * @see SeedProcessor#decodeSeedsfromRgb(ImagePlus, List, Color)
   */
  public static Seeds decodeSeedsfromRgb(final ImageProcessor rgb, final List<Color> fseed,
          final Color bseed) throws RandomWalkException {
    // output map integrating two lists of points
    Seeds out = new Seeds(2);
    // output lists of points. Can be null if points not found
    ImageProcessor background = new ByteProcessor(rgb.getWidth(), rgb.getHeight());
    // verify input condition
    if (rgb.getBitDepth() != 24) {
      throw new RandomWalkException("Unsupported seed image type");
    }
    List<Color> fseeds = new ArrayList<>(fseed);
    fseeds.add(bseed); // integrate in the same list
    // find marked pixels
    ColorProcessor cp = (ColorProcessor) rgb; // can cast here because of type checking
    for (Color color : fseeds) { // iterate over all fg (and one bg) seeds
      // foreground for current color
      ImageProcessor foreground = new ByteProcessor(rgb.getWidth(), rgb.getHeight());
      for (int x = 0; x < cp.getWidth(); x++) {
        for (int y = 0; y < cp.getHeight(); y++) {
          Color c = cp.getColor(x, y); // get color for pixel
          if (c.equals(color)) { // if current pixel has "our" color
            if (color.equals(bseed)) { // and it is from background seed
              background.putPixel(x, y, 255); // add it to background map
            } else { // otherwise store in foreground
              foreground.putPixel(x, y, 255); // remember foreground coords
            }
          }
        }
      }
      if (color.equals(bseed)) {
        out.put(RandomWalkSegmentation.SeedTypes.BACKGROUND, background); // only one background
      } else {
        // many possible foregrounds
        out.put(RandomWalkSegmentation.SeedTypes.FOREGROUNDS, foreground);
      }
    }
    // check if there is at least one seed pixel in any seed map
    validateSeeds(out, SeedTypes.FOREGROUNDS);
    validateSeeds(out, SeedTypes.BACKGROUND);

    return out;
  }

  /**
   * Decode RGB seed images to separate binary images. Support multiple foregrounds labels.
   * 
   * <p>Seeded RGB image is decomposed to separated binary images that contain only seeds.
   * E.g. <b>FOREGROUND</b> image will have only pixels that were labelled as foreground.
   * Decomposition is performed with respect to provided label colours.
   * 
   * @param rgb RGB seed image
   * @param fseed color of marker for foreground pixels
   * @param bseed color of marker for background pixels
   * @return Map containing extracted seed pixels from input RGB image that belong to foreground and
   *         background. Map is addressed by two enums: <i>FOREGROUND</i> and <i>BACKGROUND</i>
   * @throws RandomWalkException When image other that RGB provided
   * @see #decodeSeedsfromRgb(ImageProcessor, List, Color)
   */
  public static Seeds decodeSeedsfromRgb(final ImagePlus rgb, final List<Color> fseed,
          final Color bseed) throws RandomWalkException {
    if (rgb.getType() != ImagePlus.COLOR_RGB) {
      throw new RandomWalkException("Unsupported image type");
    }
    return decodeSeedsfromRgb(rgb.getProcessor(), fseed, bseed);
  }

  /**
   * Validate if at least one map in specified seed type contains non-zero pixel.
   * 
   * @param seeds seeds to verify
   * @param type seed type
   * @throws RandomWalkException when all maps under specified key are empty (black). Allows for
   *         empty maps or nonexisting keys
   */
  public static void validateSeeds(Seeds seeds, SeedTypes type) throws RandomWalkException {
    if (seeds.get(type) == null || seeds.get(type).isEmpty()) {
      return;
    }
    // check if there is at least one seed pixel in any seed map
    int pixelsNum = 0;
    int pixelHistNum = 0;
    // iterate over foregrounds
    for (ImageProcessor i : seeds.get(type)) {
      int[] histfg = i.getHistogram();
      pixelHistNum += histfg[0]; // sum number of background pixels for each map
      pixelsNum += i.getPixelCount(); // sum number of all pixels
    }
    if (pixelHistNum == pixelsNum) {
      throw new RandomWalkException(
              "Seed pixels are empty, check if:\n- correct colors were used\n- all slices have"
                      + " been seeded (if stacked seed is used)\n"
                      + "- Shrink/expand parameters are not too big.");
    }
  }

  /**
   * Decode seeds from list of ROIs objects.
   * 
   * <p>ROIs naming must comply with the following pattern: coreID_NO, where core is different for
   * FG and BG objects, ID is the id of object and NO is its number unique within ID (one object can
   * ba labelled by several separated ROIs). All ROIs must have name set otherwise
   * NullPointException is thrown.
   * 
   * @param rois list of ROIs with names
   * @param fgName core for FG ROI name
   * @param bgName core for BG core name
   * @param width width of output map
   * @param height height of output map
   * @return Seed structure with FG and BG maps.
   * @throws RandomWalkException when all seeds are empty (but maps exist)
   */
  public static Seeds decodeSeedsfromRoi(List<Roi> rois, String fgName, String bgName, int width,
          int height) throws RandomWalkException {
    Seeds ret = new Seeds();

    List<Roi> fglist = rois.stream().filter(roi -> roi.getName().startsWith(fgName))
            .collect(Collectors.toList());

    List<Roi> bglist = rois.stream().filter(roi -> roi.getName().startsWith(bgName))
            .collect(Collectors.toList());

    // process backgrounds - all rois as one ID
    ImageProcessor bg = new ByteProcessor(width, height);
    bg.setColor(Color.WHITE);
    for (Roi r : bglist) {
      r.setFillColor(Color.WHITE);
      r.setStrokeColor(Color.WHITE);
      bg.draw(r);
    }
    if (!bglist.isEmpty()) {
      ret.put(SeedTypes.BACKGROUND, bg);
    }

    ArrayList<Integer> ind = new ArrayList<>(); // array of cell numbers
    // init color for roi and collect unique cell id from name fgNameId_no
    for (Roi r : fglist) {
      r.setFillColor(Color.WHITE);
      r.setStrokeColor(Color.WHITE);
      String name = r.getName();
      String i = name.substring(fgName.length(), name.length());
      Integer n = Integer.parseInt(i.substring(0, i.indexOf("_")));
      ind.add(n);
    }
    // remove duplicates from ids
    List<Integer> norepeat = ind.stream().distinct().collect(Collectors.toList());
    Collections.sort(norepeat); // unique ROIs
    for (Integer i : norepeat) { // over unique
      ImageProcessor fg = new ByteProcessor(width, height);
      fg.setColor(Color.WHITE);
      // find all with the same No within Id and add to image
      fglist.stream().filter(roi -> roi.getName().startsWith(fgName + i))
              .forEach(roi -> fg.draw(roi));
      ret.put(SeedTypes.FOREGROUNDS, fg);
    }

    // check if there is at least one seed pixel in any seed map
    validateSeeds(ret, SeedTypes.FOREGROUNDS);
    validateSeeds(ret, SeedTypes.BACKGROUND);
    // LOGGER.debug(norepeat.toString());
    // new ImagePlus("", ret.get(0).get(SeedTypes.BACKGROUND, 1)).show();
    // List<ImageProcessor> tmp = ret.get(0).get(SeedTypes.FOREGROUNDS);
    // for (ImageProcessor ip : tmp) {
    // new ImagePlus("", ip).show();
    // }
    LOGGER.debug("Found " + norepeat.size() + " FG objects (" + fglist.size() + " total) and "
            + bglist.size() + " BG seeds");
    return ret;
  }

  /**
   * Convert the whole {@link Seeds} object into one-slice grayscale image. Background map has
   * maximum intensity.
   * 
   * <p>If there is more than one BG map it is not possible to find which of last colours are they.
   * 
   * @param seeds seeds to convert
   * @return Image with seeds in gray scale. Background is last (brightest). Null if there is no FG.
   *         Empty BG is allowed.
   * @see SeedProcessor#flatten(Seeds, SeedTypes, int)
   */
  public static ImageProcessor seedsToGrayscaleImage(Seeds seeds) {
    if (seeds.get(SeedTypes.FOREGROUNDS) == null) {
      return null;
    }
    ImageProcessor fg = flatten(seeds, SeedTypes.FOREGROUNDS, 1);
    ImageStatistics stat = fg.getStats();
    ImageProcessor bg = flatten(seeds, SeedTypes.BACKGROUND, (int) stat.max + 1);
    ImageStack stack = new ImageStack(fg.getWidth(), fg.getHeight());
    stack.addSlice(fg);
    if (bg != null) {
      stack.addSlice(bg);
    }

    ImagePlus im = new ImagePlus("", stack);
    ZProjector z = new ZProjector(im);
    z.setImage(im);
    z.setMethod(ZProjector.MAX_METHOD);
    z.doProjection();
    ImageProcessor ret = z.getProjection().getProcessor();
    return ret;
  }

  /**
   * Flatten seeds of specified type and output grayscale image.
   * 
   * @param seeds seeds to flatten
   * @param type which map
   * @param initialValue brightness value to start from (typically 1)
   * @return Image with seeds in gray scale or null if input does not contain specified map
   * @see #seedsToGrayscaleImage(Seeds)
   */
  public static ImageProcessor flatten(Seeds seeds, SeedTypes type, int initialValue) {
    if (seeds.get(type) == null) {
      return null;
    }
    int currentVal = initialValue;
    // assume same sizes of seeds
    ImageStack stack = seeds.convertToStack(type).duplicate();
    for (int s = 1; s <= stack.size(); s++) {
      stack.getProcessor(s).multiply(1.0 * currentVal / 255);
      currentVal++;
    }
    ImagePlus im = new ImagePlus("", stack);
    ZProjector z = new ZProjector(im);
    z.setImage(im);
    z.setMethod(ZProjector.MAX_METHOD);
    z.doProjection();
    ImageProcessor ret = z.getProjection().getProcessor();
    return ret;
  }

  /**
   * Convert grayscale image to {@link Seeds}.
   * 
   * <p>Pixels with the same intensity are collected in one map at {@link Seeds} structure under
   * {@link SeedTypes#FOREGROUNDS} key. Works for separated objects as
   * well. Collected maps are binary.
   * 
   * @param im 8-bit grayscale image, 0 is background
   * @return Seeds with {@link SeedTypes#FOREGROUNDS} filled. No {@link SeedTypes#BACKGROUND}
   * @throws RandomWalkException when all output FG seed maps are empty
   */
  public static Seeds decodeSeedsfromGrayscaleImage(ImageProcessor im) throws RandomWalkException {
    Seeds ret = new Seeds(2);
    ImageStatistics stats = im.getStats();
    int max = (int) stats.max; // max value
    // list of all possible theoretical values of labels in image processor except background
    // 1...max(im)
    List<Integer> lin = IntStream.rangeClosed(1, max).boxed().collect(Collectors.toList());
    ImageProcessor tmp = new ByteProcessor(im.getWidth(), im.getHeight());
    // create requested number of foreground maps
    for (int i = 0; i < max; i++) {
      ret.put(SeedTypes.FOREGROUNDS, tmp.duplicate());
    }
    // fill each map with corresponding values
    for (int r = 0; r < im.getHeight(); r++) {
      for (int c = 0; c < im.getWidth(); c++) {
        int pixel = (int) im.getPixelValue(r, c); // color of the pixel = slice in FG maps
        if (pixel > 0) { // skip background
          ret.get(SeedTypes.FOREGROUNDS).get(pixel - 1).set(r, c, Color.WHITE.getBlue());
          // remove this map number from list - for further detection gaps in grayscale seeds that
          // impose empty FG
          lin.remove(new Integer(pixel));
        }
      }
    }
    // now lin should be empty, if not it means that there are gaps and some maps were not touched
    if (!lin.isEmpty()) {
      // remove starting from end
      Collections.reverse(lin);
      for (Integer i : lin) {
        ret.get(SeedTypes.FOREGROUNDS).remove(i.intValue() - 1);
      }
    }
    validateSeeds(ret, SeedTypes.FOREGROUNDS);
    return ret;
  }

  /**
   * Convert list of ROIs to binary images separately for each ROI.
   * 
   * <p>Assumes that ROIs are named: fgNameID_NO, where ID belongs to the same object and NO are
   * different scribbles for it. Similar to
   * {@link #decodeSeedsfromRoi(List, String, String, int, int)}
   * but process each slice separately.
   *
   * @param rois rois to process.
   * @param fgName core for FG ROI name
   * @param bgName core for BG core name
   * @param width width of output map
   * @param height height of output map
   * @param slices number of slices
   * @return List of Seeds for each slice
   * @throws RandomWalkException when all seeds are empty (but maps exist)
   * @see #decodeSeedsfromRoi(List, String, String, int, int)
   */
  public static List<Seeds> decodeSeedsfromRoiStack(List<Roi> rois, String fgName, String bgName,
          int width, int height, int slices) throws RandomWalkException {
    ArrayList<Seeds> ret = new ArrayList<>();
    // find nonassigned ROIs - according to DOC getPosition() can return 0 as well (stacks start
    // from 1)
    List<Roi> col0 =
            rois.stream().filter(roi -> roi.getPosition() == 0).collect(Collectors.toList());
    // find ROIS on each slice
    for (int s = 1; s <= slices; s++) {
      final int w = s;
      List<Roi> col =
              rois.stream().filter(roi -> roi.getPosition() == w).collect(Collectors.toList());
      // merge those nonassigned to slice 1
      if (s == 1) {
        col.addAll(col0);
      }
      // produce Seeds
      Seeds tmpSeed = SeedProcessor.decodeSeedsfromRoi(col, fgName, bgName, width, height);
      ret.add(tmpSeed);
    }

    // new ImagePlus("", ret.get(0).get(SeedTypes.BACKGROUND, 1)).show();
    // List<ImageProcessor> tmp = ret.get(0).get(SeedTypes.FOREGROUNDS);
    // for (ImageProcessor ip : tmp) {
    // new ImagePlus("", ip).show();
    // }

    return ret;

  }
}