ShapeRoi.java |
package ij.gui; import java.awt.*; import java.awt.image.*; import java.awt.geom.*; import java.awt.event.KeyEvent; import java.util.*; import ij.*; import ij.process.*; import ij.measure.*; import ij.plugin.filter.Analyzer; import ij.util.Tools; import ij.util.FloatArray; /**A subclass of <code>ij.gui.Roi</code> (2D Regions Of Interest) implemented in terms of java.awt.Shape. * A ShapeRoi is constructed from a <code>ij.gui.Roi</code> object, or as a result of logical operators * (i.e., union, intersection, exclusive or, and subtraction) provided by this class. These operators use the package * <code>java.awt.geom</code> as a backend. <br> * This code is in the public domain. * @author Cezar M.Tigaret <c.tigaret@ucl.ac.uk> */ public class ShapeRoi extends Roi { /***/ static final int NO_TYPE = 128; /**The maximum tolerance allowed in calculating the length of the curve segments of this ROI's shape.*/ static final double MAXERROR = 1.0e-3; /** Coefficient used to obtain a flattened version of this ROI's shape. A flattened shape is the * closest approximation of the original shape's curve segments with line segments. * The FLATNESS is an indication of the maximum deviation between the flattened and the original shape. */ static final double FLATNESS = 0.1; /** Flatness used for filling the shape when creating a mask. Lower values result in higher accuracy * (determining which pixels near the border are filled), but lower speed when filling shapes with * curved borders. */ static final double FILL_FLATNESS = 0.01; /**Parsing a shape composed of linear segments less than this value will result in Roi objects of type * {@link ij.gui.Roi#POLYLINE} and {@link ij.gui.Roi#POLYGON} for open and closed shapes, respectively. * Conversion of shapes open and closed with more than MAXPOLY line segments will result, * respectively, in {@link ij.gui.Roi#FREELINE} and {@link ij.gui.Roi#FREEROI} (or * {@link ij.gui.Roi#TRACED_ROI} if {@link #forceTrace} flag is <strong><code>true</code></strong>. */ private static final int MAXPOLY = 10; // I hate arbitrary values !!!! private static final int OR=0, AND=1, XOR=2, NOT=3; /**The <code>java.awt.Shape</code> encapsulated by this object.*/ private Shape shape; /**The instance value of the maximum tolerance (MAXERROR) allowed in calculating the * length of the curve segments of this ROI's shape. */ private double maxerror = ShapeRoi.MAXERROR; /**The instance value of the coefficient (FLATNESS) used to * obtain a flattened version of this ROI's shape. */ private double flatness = ShapeRoi.FLATNESS; /**The instance value of MAXPOLY.*/ private int maxPoly = ShapeRoi.MAXPOLY; /**If <strong></code>true</code></strong> then methods that manipulate this ROI's shape will work on * a flattened version of the shape. */ private boolean flatten; /**Flag which specifies how Roi objects will be constructed from closed (sub)paths having more than * <code>MAXPOLY</code> and composed exclusively of line segments. * If <strong><code>true</code></strong> then (sub)path will be parsed into a * {@link ij.gui.Roi#TRACED_ROI}; else, into a {@link ij.gui.Roi#FREEROI}. */ private boolean forceTrace = false; /**Flag which specifies if Roi objects constructed from open (sub)paths composed of only two line segments * will be of type {@link ij.gui.Roi#ANGLE}. * If <strong><code>true</code></strong> then (sub)path will be parsed into a {@link ij.gui.Roi#ANGLE}; * else, into a {@link ij.gui.Roi#POLYLINE}. */ private boolean forceAngle = false; private Vector savedRois; //not really used any more private static Stroke defaultStroke = new BasicStroke(); /** Constructs a ShapeRoi from an Roi. */ public ShapeRoi(Roi r) { this(r, ShapeRoi.FLATNESS, ShapeRoi.MAXERROR, false, false, false, ShapeRoi.MAXPOLY); } /** Constructs a ShapeRoi from a Shape. */ public ShapeRoi(Shape s) { super(s.getBounds()); AffineTransform at = new AffineTransform(); at.translate(-x, -y); shape = new GeneralPath(at.createTransformedShape(s)); type = COMPOSITE; } /** Constructs a ShapeRoi from a Shape. */ public ShapeRoi(int x, int y, Shape s) { super(x, y, s.getBounds().width, s.getBounds().height); shape = new GeneralPath(s); type = COMPOSITE; } /**Creates a ShapeRoi object from a "classical" ImageJ ROI. * @param r An ij.gui.Roi object * @param flatness The flatness factor used in convertion of curve segments into line segments. * @param maxerror Error correction for calculating length of Bezeir curves. * @param forceAngle flag used in the conversion of Shape objects to Roi objects (see {@link #shapeToRois()}. * @param forceTrace flag for conversion of Shape objects to Roi objects (see {@link #shapeToRois()}. * @param flatten if <strong><code>true</code></strong> then the shape of this ROI will be flattened * (i.e., curve segments will be aproximated by line segments). * @param maxPoly Roi objects constructed from shapes composed of linear segments fewer than this * value will be of type {@link ij.gui.Roi#POLYLINE} or {@link ij.gui.Roi#POLYGON}; conversion of * shapes with linear segments more than this value will result in Roi objects of type * {@link ij.gui.Roi#FREELINE} or {@link ij.gui.Roi#FREEROI} unless the average side length * is large (see {@link #shapeToRois()}). */ ShapeRoi(Roi r, double flatness, double maxerror, boolean forceAngle, boolean forceTrace, boolean flatten, int maxPoly) { super(r.startX, r.startY, r.width, r.height); this.type = COMPOSITE; this.flatness = flatness; this.maxerror = maxerror; this.forceAngle = forceAngle; this.forceTrace = forceTrace; this.maxPoly= maxPoly; this.flatten = flatten; shape = roiToShape((Roi)r.clone()); } /** Constructs a ShapeRoi from an array of variable length path segments. Each segment consists of the segment type followed by 0-6 coordintes (0-3 end points and control points). Depending on the type, a segment uses from 1 to 7 elements of the array. */ public ShapeRoi(float[] shapeArray) { super(0,0,null); shape = makeShapeFromArray(shapeArray); Rectangle r = shape.getBounds(); x = r.x; y = r.y; width = r.width; height = r.height; state = NORMAL; oldX=x; oldY=y; oldWidth=width; oldHeight=height; AffineTransform at = new AffineTransform(); at.translate(-x, -y); shape = new GeneralPath(at.createTransformedShape(shape)); flatness = ShapeRoi.FLATNESS; maxerror = ShapeRoi.MAXERROR; maxPoly = ShapeRoi.MAXPOLY; flatten = false; type = COMPOSITE; } /**Returns a deep copy of this. */ public synchronized Object clone() { // the equivalent of "operator=" ? ShapeRoi sr = (ShapeRoi)super.clone(); sr.type = COMPOSITE; sr.flatness = flatness; sr.maxerror = maxerror; sr.forceAngle = forceAngle; sr.forceTrace = forceTrace; //sr.setImage(imp); //wsr sr.setShape(ShapeRoi.cloneShape(shape)); return sr; } /** Returns a deep copy of the argument. */ static Shape cloneShape(Shape rhs) { if (rhs==null) return null; else if (rhs instanceof Rectangle2D.Double) return (Rectangle2D.Double)((Rectangle2D.Double)rhs).clone(); else if (rhs instanceof Ellipse2D.Double) return (Ellipse2D.Double)((Ellipse2D.Double)rhs).clone(); else if (rhs instanceof Line2D.Double) return (Line2D.Double)((Line2D.Double)rhs).clone(); else if (rhs instanceof Polygon) return new Polygon(((Polygon)rhs).xpoints, ((Polygon)rhs).ypoints, ((Polygon)rhs).npoints); else if (rhs instanceof GeneralPath) return (GeneralPath)((GeneralPath)rhs).clone(); else return makeShapeFromArray(getShapeAsArray(rhs, 0, 0)); } /**********************************************************************************/ /*** Logical operations on shaped rois ****/ /**********************************************************************************/ /**Unary union operator. * The caller is set to its union with the argument. * @return the union of <strong><code>this</code></strong> and <code>sr</code> */ public ShapeRoi or(ShapeRoi sr) {return unaryOp(sr, OR);} /**Unary intersection operator. * The caller is set to its intersection with the argument (i.e., the overlapping regions between the * operands). * @return the overlapping regions between <strong><code>this</code></strong> and <code>sr</code> */ public ShapeRoi and(ShapeRoi sr) {return unaryOp(sr, AND);} /**Unary exclusive or operator. * The caller is set to the non-overlapping regions between the operands. * @return the union of the non-overlapping regions of <code>this</code> and <code>sr</code> * @see ij.gui.Roi#xor(Roi[]) * @see ij.gui.Overlay#xor(int[]) */ public ShapeRoi xor(ShapeRoi sr) {return unaryOp(sr, XOR);} /**Unary subtraction operator. * The caller is set to the result of the operation between the operands. * @return <strong><code>this</code></strong> subtracted from <code>sr</code> */ public ShapeRoi not(ShapeRoi sr) {return unaryOp(sr, NOT);} ShapeRoi unaryOp(ShapeRoi sr, int op) { AffineTransform at = new AffineTransform(); at.translate(x, y); Area a1 = new Area(at.createTransformedShape(getShape())); at = new AffineTransform(); at.translate(sr.x, sr.y); Area a2 = new Area(at.createTransformedShape(sr.getShape())); try { switch (op) { case OR: a1.add(a2); break; case AND: a1.intersect(a2); break; case XOR: a1.exclusiveOr(a2); break; case NOT: a1.subtract(a2); break; } } catch(Exception e) {} Rectangle r = a1.getBounds(); at = new AffineTransform(); at.translate(-r.x, -r.y); setShape(new GeneralPath(at.createTransformedShape(a1))); x = r.x; y = r.y; cachedMask = null; return this; } /**********************************************************************************/ /*** Interconversions between "regular" rois and shaped rois ****/ /**********************************************************************************/ /**Converts the Roi argument to an instance of java.awt.Shape. * Currently, the following conversions are supported:<br> <table><col><col><col><col><col><col><col> <thead> <tr><th scope=col> Roi class </th><th scope=col> Roi type </th><th scope=col> Shape </th><</tr> </thead> <tbody> <tr><td> ij.gui.Roi </td> <td> Roi.RECTANGLE </td><td> java.awt.geom.Rectangle2D.Double </td></tr> <tr><td> ij.gui.OvalRoi </td> <td> Roi.OVAL </td> <td> java.awt.Polygon of the corresponding traced roi </td></tr> <tr><td> ij.gui.Line </td> <td> Roi.LINE </td> <td> java.awt.geom.Line2D.Double </td></tr> <tr><td> ij.gui.PolygonRoi </td><td> Roi.POLYGON </td> <td> java.awt.Polygon or (if subpixel resolution) closed java.awt.geom.GeneralPath </td></tr> <tr><td> ij.gui.PolygonRoi </td><td> Roi.FREEROI </td> <td> java.awt.Polygon or (if subpixel resolution) closed java.awt.geom.GeneralPath </td></tr> <tr><td> ij.gui.PolygonRoi </td><td> Roi.TRACED_ROI</td><td> java.awt.Polygon or (if subpixel resolution) closed java.awt.geom.GeneralPath </td></tr> <tr><td> ij.gui.PolygonRoi </td><td> Roi.POLYLINE </td> <td> open java.awt.geom.GeneralPath </td></tr> <tr><td> ij.gui.PolygonRoi </td><td> Roi.FREELINE </td> <td> open java.awt.geom.GeneralPath </td></tr> <tr><td> ij.gui.PolygonRoi </td><td> Roi.ANGLE </td> <td> open java.awt.geom.GeneralPath </td></tr> <tr><td> ij.gui.ShapeRoi </td> <td> Roi.COMPOSITE </td><td> shape of argument </td></tr> <tr><td> ij.gui.ShapeRoi </td> <td> ShapeRoi.NO_TYPE</td><td> null </td></tr> </tbody> </table> * * @return A java.awt.geom.* object that inherits from java.awt.Shape interface. * */ private Shape roiToShape(Roi roi) { if (roi.isLine()) roi = Roi.convertLineToArea(roi); Shape shape = null; Rectangle r = roi.getBounds(); boolean closeShape = true; int roiType = roi.getType(); switch(roiType) { case Roi.LINE: Line line = (Line)roi; shape = new Line2D.Double ((double)(line.x1-r.x), (double)(line.y1-r.y), (double)(line.x2-r.x), (double)(line.y2-r.y) ); break; case Roi.RECTANGLE: int arcSize = roi.getCornerDiameter(); if (arcSize>0) shape = new RoundRectangle2D.Double(0, 0, r.width, r.height, arcSize, arcSize); else shape = new Rectangle2D.Double(0.0, 0.0, (double)r.width, (double)r.height); break; case Roi.POLYLINE: case Roi.FREELINE: case Roi.ANGLE: closeShape = false; case Roi.POLYGON: case Roi.FREEROI: case Roi.TRACED_ROI: case Roi.OVAL: if (roiType == Roi.OVAL) { //shape = new Ellipse2D.Double(-0.001, -0.001, r.width+0.002, r.height+0.002); //inaccurate (though better with increased diameter) shape = ((OvalRoi)roi).getPolygon(false); } else if (closeShape && !roi.subPixelResolution()) { int nPoints =((PolygonRoi)roi).getNCoordinates(); int[] xCoords = ((PolygonRoi)roi).getXCoordinates(); int[] yCoords = ((PolygonRoi)roi).getYCoordinates(); shape = new Polygon(xCoords, yCoords, nPoints); } else { FloatPolygon floatPoly = roi.getFloatPolygon(); if (floatPoly.npoints <=1) break; shape = new GeneralPath(closeShape ? GeneralPath.WIND_EVEN_ODD : GeneralPath.WIND_NON_ZERO, floatPoly.npoints); ((GeneralPath)shape).moveTo(floatPoly.xpoints[0] - r.x, floatPoly.ypoints[0] - r.y); for (int i=1; i<floatPoly.npoints; i++) ((GeneralPath)shape).lineTo(floatPoly.xpoints[i] - r.x, floatPoly.ypoints[i] - r.y); if (closeShape) ((GeneralPath)shape).closePath(); } break; case Roi.POINT: ImageProcessor mask = roi.getMask(); byte[] maskPixels = (byte[])mask.getPixels(); int maskWidth = mask.getWidth(); Area area = new Area(); for (int y=0; y<mask.getHeight(); y++) { int yOffset = y*maskWidth; for (int x=0; x<maskWidth; x++) { if (maskPixels[x+yOffset]!=0) area.add(new Area(new Rectangle(x, y, 1, 1))); } } shape = area; break; case Roi.COMPOSITE: shape = ShapeRoi.cloneShape(((ShapeRoi)roi).getShape()); break; default: throw new IllegalArgumentException("Roi type not supported"); } if (shape!=null) { setLocation(r.x, r.y); Rectangle2D shapeBounds = shape.getBounds2D(); Rectangle2D.Double sBounds = null; if (shapeBounds instanceof Rectangle2D.Double) sBounds = (Rectangle2D.Double)shapeBounds; else { sBounds = new Rectangle2D.Double(); sBounds.setRect(shapeBounds); //convert to Rectangle2D.Double } this.width = (int)(Math.max(sBounds.x, 0) + sBounds.width + 0.5); this.height = (int)(Math.max(sBounds.y, 0) + sBounds.height+ 0.5); if (bounds != null) { bounds.width = width; bounds.height = height; } this.startX = x; this.startY = y; //IJ.log("RoiToShape: "+x+" "+y+" "+width+" "+height+" "+bounds); } return shape; } /** Constructs a GeneralPath from a float array of segment type+coordinates for each segment. * The resulting GeneralPath has winding rule WIND_EVEN_ODD, which is appropriate for closed shapes */ static GeneralPath makeShapeFromArray(float[] array) { if(array==null) return null; GeneralPath s = new GeneralPath(GeneralPath.WIND_EVEN_ODD); int index=0; float[] seg = new float[7]; while (true) { //if(index<array.length)IJ.log(index+" type="+array[index]); index = getSegment(array, seg, index); if (index<0) break; int type = (int)seg[0]; switch(type) { case PathIterator.SEG_MOVETO: ((GeneralPath)s).moveTo(seg[1], seg[2]); break; case PathIterator.SEG_LINETO: ((GeneralPath)s).lineTo(seg[1], seg[2]); break; case PathIterator.SEG_QUADTO: ((GeneralPath)s).quadTo(seg[1], seg[2],seg[3], seg[4]); break; case PathIterator.SEG_CUBICTO: ((GeneralPath)s).curveTo(seg[1], seg[2], seg[3], seg[4], seg[5], seg[6]); break; case PathIterator.SEG_CLOSE: ((GeneralPath)s).closePath(); break; default: break; } } return s; } /** Reads the data of a segment from an array describing the shape at as in a PathIterator. Reading starts at 'index'. * The position in the array for the next segment is returned, -1 if the array ends. * The segment type and coordinates of the segment are stored into 'seg' (which must have a length of at least 7) */ private static int getSegment(float[] array, float[] seg, int index) { int len = array.length; if (index>=len) return -1; seg[0] = array[index++]; int type = (int)seg[0]; int nCoords = nCoords(type); if (index+nCoords > len) return -1; for (int i=1; i<=nCoords; i++) seg[i] = array[index++]; return index; } /** Returns the number of coordinates for the given PathIterator segment type */ private static int nCoords(int segmentType) { switch (segmentType) { case PathIterator.SEG_MOVETO: case PathIterator.SEG_LINETO: return 2; case PathIterator.SEG_QUADTO: return 4; case PathIterator.SEG_CUBICTO: return 6; case PathIterator.SEG_CLOSE: return 0; default: throw new RuntimeException("Invalid Segment Type: "+segmentType); } } /** Saves an Roi so it can be retrieved later using getRois(). Not compatible with the other functions of this class. * @deprecated Use ShapeRoi(Roi) creator and merge with <code>or(ShapeRoi)</code>. */ void saveRoi(Roi roi) { if (savedRois==null) savedRois = new Vector(); savedRois.addElement(roi); } /**Converts a Shape into Roi object(s). * <br>This method parses the shape into (possibly more than one) Roi objects * and returns them in an array. * <br>A simple, "regular" path results in a single Roi following these simple rules: <table><col><col><col> <thead><tr><th scope=col> Shape type </th><th scope=col> Roi class </th><th scope=col> Roi type </th></tr></thead> <tbody> <tr><td> java.awt.geom.Rectangle2D.Double </td><td> ij.gui.Roi </td><td> Roi.RECTANGLE </td></tr> <tr><td> java.awt.geom.Ellipse2D.Double </td><td> ij.gui.OvalRoi</td><td> Roi.OVAL </td></tr> <tr><td> java.awt.geom.Line2D.Double </td><td> ij.gui.Line </td><td> Roi.LINE </td></tr> <tr><td> java.awt.Polygon </td> <td> ij.gui.PolygonRoi </td><td> Roi.POLYGON </td></tr> </tbody> </table> * <br><br>Each subpath of a <code>java.awt.geom.GeneralPath</code> is converted following these rules: <table frame="border"><col><col><col><col><col><col> <thead> <tr><th rowspan="2" scope=col> Segment<br> types </th><th rowspan="2" scope=col> Number of<br> segments </th> <th rowspan="2" scope=col> Closed<br> path </th><th rowspan="2" scope=col> Value of<br> forceAngle </th> <th rowspan="2" scope=col> Value of<br> forceTrace </th><th rowspan="2" scope=col> Roi type </th></tr> </thead> <tbody> <tr><td> lines only: </td><td align="center"> 0 </td><td> </td><td> </td><td> </td><td> ShapeRoi.NO_TYPE </td></tr> <tr><td> </td><td align="center"> 1 </td><td> </td><td> </td><td> </td> <td> ShapeRoi.NO_TYPE </td></tr> <tr><td> </td><td align="center"> 2 </td><td align="center"> Y </td><td> </td><td> </td><td> ShapeRoi.NO_TYPE </td></tr> <tr><td> </td><td> </td><td align="center"> N </td><td> </td><td> </td><td> Roi.LINE </td></tr> <tr><td> </td><td align="center"> 3 </td><td align="center"> Y </td><td align="center"> N </td><td> </td><td> Roi.POLYGON </td></tr> <tr><td> </td><td> </td><td align="center"> N </td><td align="center"> Y </td><td> </td><td> Roi.ANGLE </td></tr> <tr><td> </td><td> </td><td align="center"> N </td><td align="center"> N </td><td> </td><td> Roi.POLYLINE </td></tr> <tr><td> </td><td align="center"> 4 </td><td align="center"> Y </td><td> </td<td> </td><td> Roi.RECTANGLE </td></tr> <tr><td> </td><td> </td><td align="center"> N </td><td> </td><td> </td><td> Roi.POLYLINE </td></tr> <tr><td> </td><td align="center"> <= MAXPOLY </td> <td align="center"> Y </td><td> </td><td> </td><td> Roi.POLYGON </td></tr> <tr><td> </td><td> </td><td align="center"> N </td><td> </td><td> </td><td> Roi.POLYLINE </td></tr> <tr><td> </td><td align="center"> > MAXPOLY </td><td align="center"> Y </td><td> </td> <td align="center"> Y </td><td> Roi.TRACED_ROI </td></tr> <tr><td> </td><td> </td><td> </td><td> </td><td align="center"> N </td><td> Roi.FREEROI </td></tr> <tr><td> </td><td> </td><td align="center"> N </td><td> </td><td> </td><td> Roi.FREELINE </td></tr> <tr><td> anything<br>else: </td><td align="center"> <= 2 </td><td> </td><td> </td><td> </td><td> ShapeRoi.NO_TYPE </td></tr> <tr><td> </td><td align="center"> > 2 </td><td> </td><td> </td><td> </td><td> ShapeRoi.SHAPE_ROI </td></tr> </tbody> </table> * @return an array of ij.gui.Roi objects. */ public Roi[] getRois () { if (shape==null) return new Roi[0]; if (savedRois!=null) return (Roi[])savedRois.toArray(new Roi[savedRois.size()]); ArrayList rois = new ArrayList(); if (shape instanceof Rectangle2D.Double) { Roi r = new Roi((int)((Rectangle2D.Double)shape).getX(), (int)((Rectangle2D.Double)shape).getY(), (int)((Rectangle2D.Double)shape).getWidth(), (int)((Rectangle2D.Double)shape).getHeight()); rois.add(r); } else if (shape instanceof Ellipse2D.Double) { Roi r = new OvalRoi((int)((Ellipse2D.Double)shape).getX(), (int)((Ellipse2D.Double)shape).getY(), (int)((Ellipse2D.Double)shape).getWidth(), (int)((Ellipse2D.Double)shape).getHeight()); rois.add(r); } else if (shape instanceof Line2D.Double) { Roi r = new ij.gui.Line((int)((Line2D.Double)shape).getX1(), (int)((Line2D.Double)shape).getY1(), (int)((Line2D.Double)shape).getX2(), (int)((Line2D.Double)shape).getY2()); rois.add(r); } else if (shape instanceof Polygon) { Roi r = new PolygonRoi(((Polygon)shape).xpoints, ((Polygon)shape).ypoints, ((Polygon)shape).npoints, Roi.POLYGON); rois.add(r); } else { PathIterator pIter; if (flatten) pIter = getFlatteningPathIterator(shape,flatness); else pIter = shape.getPathIterator(new AffineTransform()); parsePath(pIter, ALL_ROIS, rois); } return (Roi[])rois.toArray(new Roi[rois.size()]); } /**Attempts to convert this ShapeRoi into a single non-composite Roi. * @return an ij.gui.Roi object or null if it cannot be simplified to become a non-composite roi. */ public Roi shapeToRoi() { if (shape==null || !(shape instanceof GeneralPath)) return null; PathIterator pIter = shape.getPathIterator(new AffineTransform()); ArrayList rois = new ArrayList(); parsePath(pIter, ONE_ROI, rois); if (rois.size() == 1) return (Roi)rois.get(0); else return null; } /**Attempts to convert this ShapeRoi into a single non-composite Roi. * For showing as a Roi, one should apply copyAttributes * @return an ij.gui.Roi object, which is either the non-composite roi, * or this ShapeRoi (if such a conversion is not possible) or null if * this is an empty roi. */ public Roi trySimplify() { Roi roi = shapeToRoi(); return (roi==null) ? this : roi; } /**Implements the rules of conversion from <code>java.awt.geom.GeneralPath</code> to <code>ij.gui.Roi</code>. * @param nSegments The number of segments that compose the path (= number of vertices for a polygon) * @param polygonLength length of polygon in pixels, or NaN if curved segments * @param horizontalVerticalIntOnly Indicates whether the GeneralPath is composed of only vertical and horizontal lines with integer coordinates * @param forceTrace Indicates that closed shapes with <code>horizontalVerticalIntOnly=true</code> should become TRACED_ROIs * @param closed Indicates a closed GeneralPath * @see #shapeToRois() * @return a type flag like Roi.RECTANGLE or NO_TYPE if the type cannot be determined */ private int guessType(int nSegments, double polygonLength, boolean horizontalVerticalIntOnly, boolean forceTrace, boolean closed) { int roiType = Roi.RECTANGLE; if (Double.isNaN(polygonLength)) { roiType = Roi.COMPOSITE; } else { // For more segments, they should be longer to qualify for a polygon with handles: // The threshold for the average segment length is 4.0 for 4 segments, 16.0 for 64 segments, 32.0 for 256 segments boolean longEdges = polygonLength/(nSegments*Math.sqrt(nSegments)) >= 2; if (nSegments < 2) roiType = NO_TYPE; else if (nSegments == 2) roiType = closed ? NO_TYPE : Roi.LINE; else if (nSegments == 3 && !closed && forceAngle) roiType = Roi.ANGLE; else if (nSegments == 4 && closed && horizontalVerticalIntOnly && longEdges && !forceTrace && !this.forceTrace) roiType = Roi.RECTANGLE; else if (closed && horizontalVerticalIntOnly && (!longEdges || forceTrace || this.forceTrace)) roiType = Roi.TRACED_ROI; else if (nSegments <= MAXPOLY || longEdges) roiType = closed ? Roi.POLYGON : Roi.POLYLINE; else roiType = closed ? Roi.FREEROI : Roi.FREELINE; } //IJ.log("guessType n= "+nSegments+" len="+polygonLength+" longE="+(polygonLength/(nSegments*Math.sqrt(nSegments)) >= 2)+" hvert="+horizontalVerticalIntOnly+" clos="+closed+" -> "+roiType); return roiType; } /**Creates a 'classical' (non-Shape) Roi object based on the arguments. * @see #shapeToRois() * @param xPoints the x coordinates * @param yPoints the y coordinates * @param type the type flag * @return a ij.gui.Roi object or null */ private Roi createRoi(float[] xPoints, float[] yPoints, int roiType) { if (roiType == NO_TYPE || roiType == Roi.COMPOSITE) return null; Roi roi = null; if (xPoints == null || yPoints == null || xPoints.length != yPoints.length || xPoints.length==0) return null; Tools.addToArray(xPoints, (float)getXBase()); Tools.addToArray(yPoints, (float)getYBase()); switch(roiType) { case Roi.LINE: roi = new ij.gui.Line(xPoints[0],yPoints[0],xPoints[1],yPoints[1]); break; case Roi.RECTANGLE: double[] xMinMax = Tools.getMinMax(xPoints); double[] yMinMax = Tools.getMinMax(yPoints); roi = new Roi((int)xMinMax[0], (int)yMinMax[0], (int)xMinMax[1] - (int)xMinMax[0], (int)yMinMax[1] - (int)yMinMax[0]); break; case TRACED_ROI: roi = new PolygonRoi(toIntR(xPoints), toIntR(yPoints), xPoints.length, roiType); break; default: roi = new PolygonRoi(xPoints, yPoints, xPoints.length, roiType); break; } return roi; } /**********************************************************************************/ /*** Geometry ****/ /**********************************************************************************/ /** Checks whether the center of the specified pixel inside of this ROI's shape boundaries. * Note the ImageJ convention of 0.5 pixel shift between outline and pixel center, * i.e., pixel (0,0) is enclosed by the rectangle spanned between (0,0) and (1,1). * The value slightly below 0.5 is for rounding according to the ImageJ convention * (which is opposite to that of the java.awt.Shape class): * In ImageJ, points exactly at the left (right) border are considered outside (inside); * points exactly on horizontal borders, are considered outside (inside) at the border * with the lower (higher) y. */ public boolean contains(int x, int y) { if (shape==null) return false; return shape.contains(x-this.x+0.494, y-this.y+0.49994); } /** Returns whether coordinate (x,y) is contained in the Roi. * Note that the coordinate (0,0) is the top-left corner of pixel (0,0). * Use contains(int, int) to determine whether a given pixel is contained in the Roi. */ public boolean containsPoint(double x, double y) { if (!super.containsPoint(x, y)) return false; return shape.contains(x-this.x+1e-3, y-this.y+1e-6); //adding a bit to reduce the likelyhood of numerical errors at integers } /** Returns the perimeter of this ShapeRoi. */ public double getLength() { if (width==0 && height==0) return 0.0; return parsePath(shape.getPathIterator(new AffineTransform()), GET_LENGTH, null); } /** Returns a path iterator for this ROI's shape containing no curved (only straight) segments */ PathIterator getFlatteningPathIterator(Shape s, double fl) { return s.getPathIterator(new AffineTransform(),fl); } /**Length of the control polygon of the cubic Bézier curve argument, in double precision.*/ double cplength(CubicCurve2D.Double c) { return Math.sqrt(sqr(c.ctrlx1-c.x1) + sqr(c.ctrly1-c.y1)) + Math.sqrt(sqr(c.ctrlx2-c.ctrlx1) + sqr(c.ctrly2-c.ctrly1)) + Math.sqrt(sqr(c.x2-c.ctrlx2) + sqr(c.y2-c.ctrly2)); } /**Length of the control polygon of the quadratic Bézier curve argument, in double precision.*/ double qplength(QuadCurve2D.Double c) { return Math.sqrt(sqr(c.ctrlx-c.x1) + sqr(c.ctrly-c.y1)) + Math.sqrt(sqr(c.x2-c.ctrlx) + sqr(c.y2-c.ctrly)); } /**Length of the chord between the end points of the cubic Bézier curve argument, in double precision.*/ double cclength(CubicCurve2D.Double c) { return Math.sqrt(sqr(c.x2-c.x1) + sqr(c.y2-c.y1)); } /**Length of the chord between the end points of the quadratic Bézier curve argument, in double precision.*/ double qclength(QuadCurve2D.Double c) { return Math.sqrt(sqr(c.x2-c.x1) + sqr(c.y2-c.y1)); } /**Calculates the length of a cubic Bézier curve specified in double precision. * The algorithm is based on the theory presented in paper <br> * "Jens Gravesen. Adaptive subdivision and the length and energy of Bézier curves. Computational Geometry <strong>8:</strong><em>13-31</em> (1997)" * implemented using <code>java.awt.geom.CubicCurve2D.Double</code>. * Please visit {@link <a href="http://www.graphicsgems.org/gems.html#gemsiv">Graphics Gems IV</a>} for * examples of other possible implementations in C and C++. */ double cBezLength(CubicCurve2D.Double c) { double l = 0.0; double cl = cclength(c); double pl = cplength(c); if((pl-cl)/2.0 > maxerror) { CubicCurve2D.Double[] cc = cBezSplit(c); for(int i=0; i<2; i++) l+=cBezLength(cc[i]); return l; } l = 0.5*pl+0.5*cl; return l; } /**Calculates the length of a quadratic Bézier curve specified in double precision. * The algorithm is based on the theory presented in paper <br> * "Jens Gravesen. Adaptive subdivision and the length and energy of Bézier curves. Computational Geometry <strong>8:</strong><em>13-31</em> (1997)" * implemented using <code>java.awt.geom.CubicCurve2D.Double</code>. * Please visit {@link <a href="http://www.graphicsgems.org/gems.html#gemsiv">Graphics Gems IV</a>} for * examples of other possible implementations in C and C++. */ double qBezLength(QuadCurve2D.Double c) { double l = 0.0; double cl = qclength(c); double pl = qplength(c); if((pl-cl)/2.0 > maxerror) { QuadCurve2D.Double[] cc = qBezSplit(c); for(int i=0; i<2; i++) l+=qBezLength(cc[i]); return l; } l = (2.0*pl+cl)/3.0; return l; } /**Splits a cubic Bézier curve in half. * @param c A cubic Bézier curve to be divided * @return an array with the left and right cubic Bézier subcurves * */ CubicCurve2D.Double[] cBezSplit(CubicCurve2D.Double c) { CubicCurve2D.Double[] cc = new CubicCurve2D.Double[2]; for (int i=0; i<2 ; i++) cc[i] = new CubicCurve2D.Double(); c.subdivide(cc[0],cc[1]); return cc; } /**Splits a quadratic Bézier curve in half. * @param c A quadratic Bézier curve to be divided * @return an array with the left and right quadratic Bézier subcurves * */ QuadCurve2D.Double[] qBezSplit(QuadCurve2D.Double c) { QuadCurve2D.Double[] cc = new QuadCurve2D.Double[2]; for(int i=0; i<2; i++) cc[i] = new QuadCurve2D.Double(); c.subdivide(cc[0],cc[1]); return cc; } // c is an array of even length with x0, y0, x1, y1, ... ,xn, yn coordinate pairs /**Scales a coordinate array with the size calibration of a 2D image. * The array is modified in place. * @param c Array of coordinates in double precision with a <strong>fixed</strong> structure:<br> * <code>x0,y0,x1,y1,....,xn,yn</code> and with even length of <code>2*(n+1)</code>. * @param pw The x-scale of the image. * @param ph The y-scale of the image. * @param n number of values in <code>c</code> that should be modified (must be less or equal to the size of <code>c</code> * */ void scaleCoords(double[] c, int n, double pw, double ph) { for(int i=0; i<n;) { c[i++]*=pw; c[i++]*=ph; } } /** Applies an offset xBase, yBase to the n coordinates, which are supposed to be in x, y, x, y ... sequence */ static void addOffset(float[] c, int n, float xBase, float yBase) { for(int i=0; i<n;) { c[i++] += xBase; c[i++] += yBase; } } /** Retrieves the end points and control points of the path as a float array. The array * contains a sequence of variable length segments that use from from one to seven array elements. * The first element of a segment is the type as defined in the PathIterator interface. SEG_MOVETO * and SEG_LINETO segments also include two coordinates (one end point), SEG_QUADTO segments include four * coordinates and SEG_CUBICTO segments include six coordinates (three points). * Coordinates are with respect to the image bounds, not the Roi bounds. */ public float[] getShapeAsArray() { return getShapeAsArray(shape, (float)getXBase(), (float)getYBase()); } /** Converts a java.awt.Shape to an array of segment types and coordinates. * 'xBase' and 'yBase' are added to the x and y coordinates, respectively. */ static float[] getShapeAsArray(Shape shape, float xBase, float yBase) { if (shape==null) return null; PathIterator pIt = shape.getPathIterator(new AffineTransform()); FloatArray shapeArray = new FloatArray(); float[] coords = new float[6]; while (!pIt.isDone()) { int segType = pIt.currentSegment(coords); shapeArray.add(segType); int nCoords = nCoords(segType); if (nCoords > 0) { addOffset(coords, nCoords, xBase, yBase); shapeArray.add(coords, nCoords); } pIt.next(); } return shapeArray.toArray(); } final static int ALL_ROIS=0, ONE_ROI=1, GET_LENGTH=2; //task types final static int NO_SEGMENT_ANY_MORE = -1; //pseudo segment type when closed /**Parses the geometry of this ROI's shape by means of the shape's PathIterator; * Depending on the <code>task</code> and <code>rois</code> argument it will: * <br>- create a single non-Shape Roi and add it to <code>rois</code> in case * there is only one subpath, otherwise add this Roi unchanged to <code>rois</code> * (task = ONE_ROI and rois non-null) * <br>- add each subpath as a Roi to rois; curved subpaths will be flattened, i.e. converted to a * polygon approximation (task != ONE_ROI and rois non-null) * <br>- measure the combined length of all subpaths/Rois and return it (task = GET_LENGTH, rois may be null) * @param pIter the PathIterator to be parsed. * @param params an array with one element that will hold the calculated total length of the rois if its initial value is 0. * If params holds the value SHAPE_TO_ROI, it will be tried to convert this ShapeRoi to a non-composite Roi. If this * is not possible and this ShapeRoi is not empty, a reference to this ShapeRoi will be returned. * @param rois an ArrayList that will hold ij.gui.Roi objects constructed from subpaths of this path; * may be null only when <code>task = GET_LENGTH</code> * (see @link #shapeToRois()} for details; * @return Total length if task = GET_LENGTH.*/ double parsePath(PathIterator pIter, int task, ArrayList rois) { if (pIter==null || pIter.isDone()) return 0.0; double pw = 1.0, ph = 1.0; if (imp!=null) { Calibration cal = imp.getCalibration(); pw = cal.pixelWidth; ph = cal.pixelHeight; } float xbase = (float)getXBase(); float ybase = (float)getYBase(); FloatArray xPoints = new FloatArray(); //vertex coordinates of current subpath FloatArray yPoints = new FloatArray(); FloatArray shapeArray = new FloatArray(); //values for creating a GeneralPath for the current subpath boolean getLength = task == GET_LENGTH; int nSubPaths = 0; // the number of subpaths boolean horVertOnly = true; // subpath has only horizontal or vertical lines boolean closed = false; //boolean success = false; float[] fcoords = new float[6]; // unscaled float coordinates of the path segment double[] coords = new double[6]; // scaled (calibrated) coordinates of the path segment double startCalX = 0.0; // start x of subpath (scaled) double startCalY = 0.0; // start y of subpath (scaled) double lastCalX = 0.0; // x of previous point in the subpath (scaled) double lastCalY = 0.0; // y of previous point in the subpath (scaled) double pathLength = 0.0; // calibrated pathLength/perimeter of current curve double totalLength = 0.0; // sum of all calibrated path lengths/perimeters double uncalLength = 0.0; // uncalibrated length of polygon, NaN in case of curves boolean done = false; while (true) { int segType = done ? NO_SEGMENT_ANY_MORE : pIter.currentSegment(fcoords); //read segment (if there is one more) int nCoords = 0; //will be number of coordinates supplied with the segment if (!done) { nCoords = nCoords(segType); if (getLength) { //make scaled coodinates to calculate the length pIter.currentSegment(coords); scaleCoords(coords, nCoords, pw, ph); } pIter.next(); done = pIter.isDone(); } //IJ.log("segType="+segType+" nCoord="+nCoords+" done="+done+" nPoi="+nPoints+" len="+pathLength); if (segType == NO_SEGMENT_ANY_MORE || (segType == PathIterator.SEG_MOVETO && xPoints.size()>0)) { // subpath finished: analyze it & create roi if appropriate closed = closed || (xPoints.size()>0 && xPoints.get(0) == xPoints.getLast() && yPoints.get(0) == yPoints.getLast()); float[] xpf = xPoints.toArray(); float[] ypf = yPoints.toArray(); if (Double.isNaN(uncalLength) || !allInteger(xpf) || !allInteger(ypf)) horVertOnly = false; //allow conversion to rectangle or traced roi only for integer coordinates boolean forceTrace = getLength && (!done || nSubPaths>0); //when calculating the length for >1 subpath, assume traced rois if it can be such int roiType = guessType(xPoints.size(), uncalLength, horVertOnly, forceTrace, closed); Roi roi = null; if (roiType == COMPOSITE && rois != null) { //for ShapeRois with curves, we have the length from the path already, make roi only if needed Shape shape = makeShapeFromArray(shapeArray.toArray()); //the curved subpath (image pixel coordinates) FloatPolygon fp = getFloatPolygon(shape, FLATNESS, /*separateSubpaths=*/ false, /*addPointForClose=*/ false, /*absoluteCoord=*/ false); roi = new PolygonRoi(fp, FREEROI); } else if (roiType != NO_TYPE) { //NO_TYPE looks like an empty roi; only return non-empty rois roi = createRoi(xpf, ypf, roiType); } if (rois != null && roi != null) rois.add(roi); if (task == ONE_ROI) { if (rois.size() > 1) { //we can't make a single roi from this; so we can only keep the roi as it is rois.clear(); rois.add(this); return 0.0; } } if (getLength && roi != null && !Double.isNaN(uncalLength)) { roi.setImage(imp); //calibration pathLength = roi.getLength();//we don't use the path length of the Shape; e.g. for traced rois ImageJ has a better algorithm roi.setImage(null); } totalLength += pathLength; } if (segType == NO_SEGMENT_ANY_MORE) // b r e a k t h e l o o p return getLength ? totalLength : 0; closed = false; switch(segType) { case PathIterator.SEG_MOVETO: //we start a new subpath xPoints.clear(); yPoints.clear(); shapeArray.clear(); nSubPaths++; pathLength = 0; startCalX = coords[0]; startCalY = coords[1]; closed = false; horVertOnly = true; break; case PathIterator.SEG_LINETO: pathLength += Math.sqrt(sqr(lastCalY-coords[1])+sqr(lastCalX-coords[0])); break; case PathIterator.SEG_QUADTO: if (getLength) { QuadCurve2D.Double curve = new QuadCurve2D.Double(lastCalX,lastCalY,coords[0],coords[2],coords[2],coords[3]); pathLength += qBezLength(curve); } uncalLength = Double.NaN; // not a polygon break; case PathIterator.SEG_CUBICTO: if (getLength) { CubicCurve2D.Double curve = new CubicCurve2D.Double(lastCalX,lastCalY,coords[0],coords[1],coords[2],coords[3],coords[4],coords[5]); pathLength += cBezLength(curve); } uncalLength = Double.NaN; // not a polygon break; case PathIterator.SEG_CLOSE: pathLength += Math.sqrt(sqr(lastCalX-startCalX) + sqr(lastCalY-startCalY)); fcoords[0] = xPoints.get(0); //destination coordinates; with these we can handle it as SEG_LINETO fcoords[1] = yPoints.get(0); closed = true; break; default: break; } if (xPoints.size()>0 && (segType == PathIterator.SEG_LINETO || segType == PathIterator.SEG_CLOSE)) { float dx = fcoords[0] - xPoints.getLast(); float dy = fcoords[1] - yPoints.getLast(); uncalLength += Math.sqrt(sqr(dx) + sqr(dy)); if (dx != 0f && dy != 0f) horVertOnly = false; } if (nCoords > 0) { xPoints.add(fcoords[nCoords - 2]); // the last coordinates are the end point of the segment yPoints.add(fcoords[nCoords - 1]); lastCalX = coords[nCoords - 2]; lastCalY = coords[nCoords - 1]; } shapeArray.add(segType); addOffset(fcoords, nCoords, xbase, ybase); // shift the shape to image origin shapeArray.add(fcoords, nCoords(segType)); } } /** Non-destructively draws the shape of this object on the associated ImagePlus. */ public void draw(Graphics g) { Color color = strokeColor!=null? strokeColor:ROIColor; boolean isActiveOverlayRoi = !overlay && isActiveOverlayRoi(); //IJ.log("draw: "+overlay+" "+isActiveOverlayRoi); if (isActiveOverlayRoi) { if (color==Color.cyan) color = Color.magenta; else color = Color.cyan; } if (fillColor!=null) color = fillColor; g.setColor(color); AffineTransform aTx = (((Graphics2D)g).getDeviceConfiguration()).getDefaultTransform(); Graphics2D g2d = (Graphics2D)g; if (stroke!=null && !isActiveOverlayRoi) g2d.setStroke((ic!=null&&ic.getCustomRoi())||isCursor()?stroke:getScaledStroke()); mag = getMagnification(); int basex=0, basey=0; if (ic!=null) { Rectangle r = ic.getSrcRect(); basex=r.x; basey=r.y; } aTx.setTransform(mag, 0.0, 0.0, mag, -basex*mag, -basey*mag); aTx.translate(getXBase(), getYBase()); if (fillColor!=null) { if (isActiveOverlayRoi) { g2d.setColor(Color.cyan); g2d.draw(aTx.createTransformedShape(shape)); } else g2d.fill(aTx.createTransformedShape(shape)); } else g2d.draw(aTx.createTransformedShape(shape)); if (stroke!=null) g2d.setStroke(defaultStroke); if (Toolbar.getToolId()==Toolbar.OVAL) drawRoiBrush(g); if (state!=NORMAL && imp!=null && imp.getRoi()!=null) showStatus(); if (updateFullWindow) {updateFullWindow = false; imp.draw();} } public void drawRoiBrush(Graphics g) { g.setColor(ROIColor); int size = Toolbar.getBrushSize(); if (size==0 || ic==null) return; int flags = ic.getModifiers(); if ((flags&16)==0) return; // exit if mouse button up int osize = size; size = (int)(size*mag); Point p = ic.getCursorLoc(); int sx = ic.screenX(p.x); int sy = ic.screenY(p.y); int offset = (int)Math.round(ic.getMagnification()/2.0); if ((osize&1)==0) offset=0; // not needed when brush width even g.drawOval(sx-size/2+offset, sy-size/2+offset, size, size); } /**Draws the shape of this object onto the specified ImageProcessor. * <br> This method will always draw a flattened version of the actual shape * (i.e., all curve segments will be approximated by line segments). */ public void drawPixels(ImageProcessor ip) { PathIterator pIter = shape.getPathIterator(new AffineTransform(), flatness); float[] coords = new float[6]; float sx=0f, sy=0f; while (!pIter.isDone()) { int segType = pIter.currentSegment(coords); switch(segType) { case PathIterator.SEG_MOVETO: sx = coords[0]; sy = coords[1]; ip.moveTo(x+(int)sx, y+(int)sy); break; case PathIterator.SEG_LINETO: ip.lineTo(x+(int)coords[0], y+(int)coords[1]); break; case PathIterator.SEG_CLOSE: ip.lineTo(x+(int)sx, y+(int)sy); break; default: break; } pIter.next(); } } /** Returns this ROI's mask pixels as a ByteProcessor with pixels "in" the mask * set to white (255) and pixels "outside" the mask set to black (0). * Takes into account the usual ImageJ convention of 0.5 pxl shift between the outline and pixel * coordinates; e.g., pixel (0,0) is surrounded by the rectangle spanned between (0,0) and (1,1). * Note that apart from the 0.5 pixel shift, ImageJ has different convention for the border points * than the java.awt.Shape class: * In ImageJ, points exactly at the left (right) border are considered outside (inside); * points exactly on horizontal borders, are considered outside (inside) at the border * with the lower (higher) y. * */ public ImageProcessor getMask() { if (shape==null) return null; ImageProcessor mask = cachedMask; if (mask!=null && mask.getPixels()!=null && mask.getWidth()==width && mask.getHeight()==height) return mask; /* The following code using Graphics2D.fill would in principle work, but is very inaccurate * at least with Oracle Java 8 or OpenJDK Java 10. * For near-vertical polgon edges of 1000 pixels length, the deviation can be >0.8 pixels in x. * Thus, approximating the shape by a polygon and using the PolygonFiller is more accurate * (and roughly equally fast). --ms Jan 2018 */ /*BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); Graphics2D g2d = bi.createGraphics(); g2d.setColor(Color.white); g2d.transform(AffineTransform.getTranslateInstance(-0.48, -0.49994)); //very inaccurate, only reasonable with "-0.48" g2d.fill(shape); Raster raster = bi.getRaster(); DataBufferByte buffer = (DataBufferByte)raster.getDataBuffer(); byte[] mask = buffer.getData(); cachedMask = new ByteProcessor(width, height, mask, null); cachedMask.setThreshold(255,255,ImageProcessor.NO_LUT_UPDATE);*/ FloatPolygon fpoly = getFloatPolygon(FILL_FLATNESS, true, false, false); PolygonFiller pf = new PolygonFiller(fpoly.xpoints, fpoly.ypoints, fpoly.npoints, (float)(getXBase()-x), (float)(getYBase()-y)); mask = pf.getMask(width, height); cachedMask = mask; return mask; } /**Returns a reference to the Shape object encapsulated by this ShapeRoi. */ public Shape getShape() { return shape; } /**Sets the <code>java.awt.Shape</code> object encapsulated by <strong><code>this</code></strong> * to the argument. * <br>This object will hold a (shallow) copy of the shape argument. If a deep copy * of the shape argumnt is required, then a clone of the argument should be passed * in; a possible example is <code>setShape(ShapeRoi.cloneShape(shape))</code>. * @return <strong><code>false</code></strong> if the argument is null. */ boolean setShape(Shape rhs) { boolean result = true; if (rhs==null) return false; if (shape.equals(rhs)) return false; shape = rhs; type = Roi.COMPOSITE; Rectangle rect = shape.getBounds(); width = rect.width; height = rect.height; return true; } /**Returns the element with the smallest value in the array argument.*/ private int min(int[] array) { int val = array[0]; for (int i=1; i<array.length; i++) val = Math.min(val,array[i]); return val; } /**Returns the element with the largest value in the array argument.*/ private int max(int[] array) { int val = array[0]; for (int i=1; i<array.length; i++) val = Math.max(val,array[i]); return val; } static ShapeRoi getCircularRoi(int x, int y, int width) { return new ShapeRoi(new OvalRoi(x - width / 2, y - width / 2, width, width)); } /** Always returns -1 since ShapeRois do not have handles. */ public int isHandle(int sx, int sy) { return -1; } /** Used by the getSelectionCoordinates macro function */ public FloatPolygon getSelectionCoordinates() { return getFloatPolygon(FLATNESS, true, false, true); } /** Returns a FloatPolygon with all vertices of the flattened shape, i.e., the shape approximated by * straight line segments. This method is for listing the coordinates and creating the convex hull. * @param flatness Roughly the maximum allowable distance between the shape and the approximate polygon * @param separateSubpaths whether individual subpaths should be separated by NaN coordinates * @param addPointForClose whether the starting point of a closed subpath should be repeated at its end. * Note that with <code>addPointForClose = false</code>, there is no distinction between open and closed subpaths. * @param absoluteCoord specifies whether the coordinates should be with respect to image bounds, not Roi bounds. */ public FloatPolygon getFloatPolygon(double flatness, boolean separateSubpaths, boolean addPointForClose, boolean absoluteCoord) { return getFloatPolygon(shape, flatness, separateSubpaths, addPointForClose, absoluteCoord); } public FloatPolygon getFloatPolygon(Shape shape, double flatness, boolean separateSubpaths, boolean addPointForClose, boolean absoluteCoord) { if (shape == null) return null; PathIterator pIter = getFlatteningPathIterator(shape, flatness); FloatArray xp = new FloatArray(); FloatArray yp = new FloatArray(); float[] coords = new float[6]; int subPathStart = 0; while (!pIter.isDone()) { int segType = pIter.currentSegment(coords); switch(segType) { case PathIterator.SEG_MOVETO: if (separateSubpaths && xp.size()>0 && !Float.isNaN(xp.get(xp.size()-1))) { xp.add(Float.NaN); yp.add(Float.NaN); } subPathStart = xp.size(); case PathIterator.SEG_LINETO: xp.add(coords[0]); yp.add(coords[1]); break; case PathIterator.SEG_CLOSE: boolean isClosed = xp.getLast() == xp.get(subPathStart) && yp.getLast() == yp.get(subPathStart); if (addPointForClose && !isClosed) { xp.add(xp.get(subPathStart)); yp.add(yp.get(subPathStart)); } else if (isClosed) { xp.removeLast(1); //remove duplicate point if we should not add point to close the shape yp.removeLast(1); } if (separateSubpaths && xp.size()>0 && !Float.isNaN(xp.get(xp.size()-1))) { xp.add(Float.NaN); yp.add(Float.NaN); } break; default: throw new RuntimeException("Invalid Segment Type: "+segType); } pIter.next(); } float[] xpf = xp.toArray(); float[] ypf = yp.toArray(); if (absoluteCoord) { Tools.addToArray(xpf, (float)getXBase()); Tools.addToArray(ypf, (float)getYBase()); } int n = xpf.length; if (n>0 && Float.isNaN(xpf[n-1])) n--; //omit NaN at the end return new FloatPolygon(xpf, ypf, n); } public FloatPolygon getFloatConvexHull() { FloatPolygon fp = getFloatPolygon(FLATNESS, /*separateSubpaths=*/ false, /*addPointForClose=*/ false, /*absoluteCoord=*/ true); return fp == null ? null : fp.getConvexHull(); } public Polygon getPolygon() { FloatPolygon fp = getFloatPolygon(); return new Polygon(toIntR(fp.xpoints), toIntR(fp.ypoints), fp.npoints); } /** Returns all vertex points of the shape as approximated by polygons, * in image pixel coordinates */ public FloatPolygon getFloatPolygon() { return getFloatPolygon(FLATNESS, /*separateSubpaths=*/ false, /*addPointForClose=*/ false, /*absoluteCoord=*/ true); } /** Returns all vertex points of the shape as approximated by polygons, * where options may include "close" to add points to close each subpath, and * "separate" to insert NaN values between subpaths (= individual polygons) */ public FloatPolygon getFloatPolygon(String options) { options = options.toLowerCase(); boolean separateSubpaths = options.indexOf("separate") >= 0; boolean addPointForClose = options.indexOf("close") >= 0; return getFloatPolygon(FLATNESS, separateSubpaths, addPointForClose, /*absoluteCoord=*/ true); } /** Retuns the number of vertices, of this shape as approximated by straight lines. * Note that points might be counted twice where the shape gets closed. */ public int size() { return getPolygon().npoints; } boolean allInteger(float[] a) { for (int i=0; i<a.length; i++) if (a[i] != (int)a[i]) return false; return true; } }