TrackOutline.java
package com.github.celldynamics.quimp.geom;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.scijava.vecmath.Point2d;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.celldynamics.quimp.Outline;
import com.github.celldynamics.quimp.plugin.utils.QuimpDataConverter;
import ij.ImagePlus;
import ij.gui.PolygonRoi;
import ij.gui.Roi;
import ij.gui.Wand;
import ij.process.ImageProcessor;
/*
* //!>
* @startuml doc-files/TrackOutline_1_UML.png
* User->(Create object)
* User->(Convert Outlines to Point2d)
* User->(get deep copy of Outlines)
* @enduml
*
* @startuml doc-files/TrackOutline_2_UML.png
* actor User
* User-->TrackOutline : <<create>>\n""image"",""frame""
* TrackOutline->prepare : ""image""
* note left
* Filtering and BW
* operations
* endnote
* prepare -> prepare : //open//
* prepare->prepare : //close//
* prepare->TrackOutline : ""prepared""
* TrackOutline -> getOutlines
* loop every pixel
* getOutlines->getOutline : not background pixel [x,y]
* getOutline->Wand : [x,y]
* Wand->getOutline : ""xpoints"",""ypoints""
* getOutline->SegmentedShapeRoi : <<create>>
* SegmentedShapeRoi-->getOutline
* getOutline->getOutline : clear ROI on image
* getOutline->SegmentedShapeRoi : set ""frame""
* SegmentedShapeRoi-->getOutline
* getOutline->getOutlines : ""SegmentedShapeRoi""
* getOutlines->getOutlines : store ""SegmentedShapeRoi""
* getOutlines->getOutlines : store ""Color""
* end
* getOutlines->TrackOutline
* @enduml
*
* //!<
*/
/**
* Convert grayscale masks into list of vertices in correct order. Stand as ROI holder.
*
* <p>The algorithm uses IJ tools for tracking and filling (deleting) objects It goes through all
* points of the image and for every visited point it checks whether the value is different than
* defined background. If it is, the Wand tool is used to select object given by the pixel value
* inside it. The ROI (outline) is then stored in this object and served as reference The ROI is
* then used to delete selected object from image (using background value). Next, the algorithm
* moves to next pixel (of the same image the object has been deleted from, so it is not possible to
* detect the same object twice).
*
* <p>It assigns also frame number to outline<br>
* <img src="doc-files/TrackOutline_1_UML.png"/><br>
* Creating object runs also outline detection and tracking. Detected outlines are stored in object
* and can be accessed by reference directly from outlines array or as copies from
* getCopyofShapes().<br>
* <img src="doc-files/TrackOutline_2_UML.png"/><br>
*
* @author p.baniukiewicz
* @see com.github.celldynamics.quimp.geom.SegmentedShapeRoi
*
*/
public class TrackOutline {
/**
* The Constant LOGGER.
*/
static final Logger LOGGER = LoggerFactory.getLogger(TrackOutline.class.getName());
/**
* ROIs below this size (width or height) will be removed.
*
* @see #getOutlines(double, boolean)
* @see #getOutlinesasPoints(double, boolean)
* @see #getOutlinesColors(double, boolean)
* @see #getOutlineasRawPoints()
*/
static final int SIZE_LIMIT = 10;
/**
* Original image. It is not modified.
*/
protected ImageProcessor imp;
/**
* Image under process. It is modified by Outline methods.
*/
private ImageProcessor prepared;
/**
* List of found outlines as ROIs.
*/
public ArrayList<SegmentedShapeRoi> outlines;
/**
* List of colors of objects that were used to produce SegmentedShapeRoi.
*
* <p>This list is related to {@link TrackOutline#outlines}. Colors are encoded as rgb
* {@link Color#Color(int)}
*/
public ArrayList<Color> colors;
/**
* The background color.
*/
protected int background;
/**
* Maximal number of searched objects, all objects if negative.
*/
private int maxNumObj = -1;
/**
* Frame for which imp has been got.
*/
private int frame;
/**
* Constructor from ImageProcessor.
*
* @param imp Image to process (not modified)
* @param background Color value for background
* @param frame Frame of stack that \a imp belongs to
* @throws IllegalArgumentException when wrong image format is provided
*/
public TrackOutline(ImageProcessor imp, int background, int frame) {
if (imp.getBitDepth() != 8 && imp.getBitDepth() != 16) {
throw new IllegalArgumentException("Only 8-bit or 16-bit images are supported");
}
outlines = new ArrayList<>();
colors = new ArrayList<>();
this.imp = imp;
this.background = background;
this.prepared = prepare();
this.frame = frame;
getOutlines();
}
/**
* Constructor from ImageProcessor for single images.
*
* @param imp Image to process (not modified)
* @param background Color value for background
* @throws IllegalArgumentException when wrong image format is provided
*/
public TrackOutline(ImageProcessor imp, int background) {
this(imp, background, 1);
}
/**
* Default constructor.
*
* @param im Image to process (not modified), 8-bit, one slice
* @param background Background color
*/
public TrackOutline(ImagePlus im, int background) {
this(im.getProcessor(), background, 1);
}
/**
* Filter input image to remove single pixels.
*
* <p>Implement closing followed by opening.
*
* <P>TODO If input image is grayscale this method may not work as expected, e.g. small pixels
* with one color sticked to larger objects with another color will not be removed
*
* @return Filtered processor
*/
public ImageProcessor prepare() {
ImageProcessor filtered = imp.duplicate();
// closing
filtered.dilate();
filtered.erode();
// opening
filtered.erode();
filtered.dilate();
return filtered;
}
/**
* Get outline using Wand tool.
*
* @param col Any point inside region
* @param row Any point inside region
* @param color Color of object
* @return ShapeRoi that contains ROI for given object with assigned frame to it
* @throws IllegalArgumentException when wand was not able to find point
*/
SegmentedShapeRoi getOutline(int col, int row, int color) {
Wand wand = new Wand(prepared);
wand.autoOutline(col, row, color, color, Wand.EIGHT_CONNECTED);
if (wand.npoints == 0) {
throw new IllegalArgumentException("Wand: Points not found");
}
Roi roi = new PolygonRoi(wand.xpoints, wand.ypoints, wand.npoints, Roi.FREEROI);
clearRoi(roi, background);
SegmentedShapeRoi ret = new SegmentedShapeRoi(roi); // create segmentation object
ret.setFrame(frame); // set current frame to this object
return ret;
}
/**
* Try to find all outlines on image.
*
* <p>It is possible to limit number of searched outlines setting maxNumObj > 0 The algorithm goes
* through every pixel on image and if this pixel is different than background (defined in
* constructor) it uses it as source of Wand. Wand should outline found object, which is then
* erased from image. then next pixel is analyzed.
*
* <p>Fills outlines field that contains list of all ROIs obtained for this image together with
* frame number assigned to TrackOutline.
*
* <p>Skip very small object.
*
* @see #SIZE_LIMIT
*
*/
private void getOutlines() {
// go through the image and look for non background pixels
outer: for (int r = 0; r < prepared.getHeight(); r++) {
for (int c = 0; c < prepared.getWidth(); c++) {
int pixel = prepared.getPixel(c, r);
if (pixel != background) { // non background pixel
// remember outline and delete it from input image
SegmentedShapeRoi sr = getOutline(c, r, pixel);
if (sr.getFloatWidth() < SIZE_LIMIT || sr.getFloatHeight() < SIZE_LIMIT) {
continue;
}
outlines.add(sr);
colors.add(new Color(pixel)); // store source color as rgb
if (maxNumObj > -1) {
if (outlines.size() >= maxNumObj) {
LOGGER.warn("Reached maximal number of outlines");
break outer;
}
}
}
}
}
}
/**
* Convert found outlines to Outline.
*
* @param step resolution step
* @param smooth true to use IJ polygon smoothing (running average).
* @return List of Outline object that represents all
* @see SegmentedShapeRoi#getOutlineasPoints()
* @see #getOutlinesasPoints(double, boolean)
* @see #getOutlinesColors(double, boolean)
*/
public List<Outline> getOutlines(double step, boolean smooth) {
Pair<List<Outline>, List<Color>> ret = getOutlinesColors(step, smooth);
return ret.getLeft();
}
/**
* Convert found outlines to Outline.
*
* @param step resolution step
* @param smooth true to use IJ polygon smoothing (running average).
* @return List of Outline object and colors of foreground pixels used to produce them coded as
* rgb by {@link Color#Color(int)}
* @see SegmentedShapeRoi#getOutlineasPoints()
* @see #getOutlinesasPoints(double, boolean)
*/
public Pair<List<Outline>, List<Color>> getOutlinesColors(double step, boolean smooth) {
List<SegmentedShapeRoi> rois = getCopyofShapes();
// convert to Outlines from ROIs
ArrayList<Outline> outlines = new ArrayList<>();
for (SegmentedShapeRoi sr : rois) {
// interpolate and reduce number of points
sr.setInterpolationParameters(step, false, smooth);
Outline o;
o = new QuimpDataConverter(sr.getOutlineasPoints()).getOutline();
outlines.add(o);
}
return new ImmutablePair<List<Outline>, List<Color>>(outlines, colors);
}
/**
* Reformat collected outlines and Colors to list of pairs.
*
* @param step resolution step
* @param smooth true to use IJ polygon smoothing (running average).
* @return List of pairs, outlines and colors of pixels they were created from
*/
public List<Pair<Outline, Color>> getPairs(double step, boolean smooth) {
List<Outline> out = getOutlinesColors(step, smooth).getLeft();
List<Pair<Outline, Color>> ret = new ArrayList<>();
Iterator<Outline> ito = out.iterator();
Iterator<Color> itc = colors.iterator();
while (ito.hasNext() && itc.hasNext()) {
Pair<Outline, Color> p = new ImmutablePair<Outline, Color>(ito.next(), itc.next());
ret.add(p);
}
return ret;
}
/**
* Erase roi on image stored in object with color bckColor.
*
* @param roi roi on this image
* @param bckColor color for erasing
*/
private void clearRoi(Roi roi, int bckColor) {
prepared.setColor(bckColor);
prepared.fill(roi);
}
/**
* Convert found outlines to List.
*
* @param step step - step during conversion outline to points. For 1 every point from outline
* is included in output list
* @param smooth true for using running average during interpolation
* @return List of List of ROIs
* @see SegmentedShapeRoi#getOutlineasPoints()
*/
public List<List<Point2d>> getOutlinesasPoints(double step, boolean smooth) {
List<List<Point2d>> ret = new ArrayList<>();
for (SegmentedShapeRoi sr : outlines) {
sr.setInterpolationParameters(step, false, smooth);
ret.add(sr.getOutlineasPoints());
}
return ret;
}
/**
* Convert found outlines to List without any interpolation.
*
* @return List of List of ROIs
*
* @see SegmentedShapeRoi#getOutlineasPoints()
*/
public List<List<Point2d>> getOutlineasRawPoints() {
List<List<Point2d>> ret = new ArrayList<>();
for (SegmentedShapeRoi sr : outlines) {
ret.add(sr.getOutlineasRawPoints());
}
return ret;
}
/**
* Return deep copy of Shapes.
*
* @return deep copy of Rois.
*/
public List<SegmentedShapeRoi> getCopyofShapes() {
ArrayList<SegmentedShapeRoi> clon = new ArrayList<>();
for (SegmentedShapeRoi sr : outlines) {
clon.add((SegmentedShapeRoi) sr.clone());
}
return clon;
}
/**
* Return shallow copy of Shapes.
*
* @return Rois found on image.
*
* @see #getColors()
*/
public List<SegmentedShapeRoi> getShapes() {
return outlines;
}
/**
* Get colors of pixels that outlines were produced from.
*
* <p>Size of this array and order of elements correspond to {@link #getShapes()} and all
* get methods in this class.
*
* @return the colors as RGB, created by {@link Color#Color(int)}. Integer can be retrieved by
* summing up three RGB components.
*
* @see #getShapes()
*/
public ArrayList<Color> getColors() {
return colors;
}
/**
* Set {@link Roi#setStrokeColor(Color)} of each found Roi to color of pixels it was produced
* from.
*
* <p>Modified are {@link SegmentedShapeRoi} stored in {@link #outlines}
*/
public void setColors() {
Iterator<SegmentedShapeRoi> ito = outlines.iterator();
Iterator<Color> itc = colors.iterator();
while (ito.hasNext() && itc.hasNext()) {
ito.next().setStrokeColor(itc.next());
}
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "\nTrackOutline [outlines=" + outlines + "]";
}
}