All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.alg.shapes.polygon.BinaryPolygonDetector Maven / Gradle / Ivy

Go to download

BoofCV is an open source Java library for real-time computer vision and robotics applications.

There is a newer version: 0.26
Show newest version
/*
 * Copyright (c) 2011-2016, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.alg.shapes.polygon;

import boofcv.alg.InputSanityCheck;
import boofcv.alg.distort.DistortImageOps;
import boofcv.alg.filter.binary.Contour;
import boofcv.alg.filter.binary.LinearContourLabelChang2004;
import boofcv.alg.shapes.edge.PolygonEdgeIntensity;
import boofcv.alg.shapes.polyline.MinimizeEnergyPrune;
import boofcv.alg.shapes.polyline.RefinePolyLineCorner;
import boofcv.alg.shapes.polyline.SplitMergeLineFitLoop;
import boofcv.struct.ConnectRule;
import boofcv.struct.distort.PixelTransform_F32;
import boofcv.struct.image.ImageSInt32;
import boofcv.struct.image.ImageSingleBand;
import boofcv.struct.image.ImageUInt8;
import georegression.geometry.UtilPolygons2D_F64;
import georegression.metric.Area2D_F64;
import georegression.struct.point.Point2D_F64;
import georegression.struct.point.Point2D_I32;
import georegression.struct.shapes.Polygon2D_F64;
import georegression.struct.shapes.RectangleLength2D_F32;
import org.ddogleg.struct.FastQueue;
import org.ddogleg.struct.GrowQueue_B;
import org.ddogleg.struct.GrowQueue_I32;

import java.util.ArrayList;
import java.util.List;

/**
 * 

* Detects convex polygons with the specified number of sides in an image. Shapes are assumed to be black shapes * against a white background, allowing for thresholding to be used. Subpixel refinement is done using the * provided implementation of {@link RefineBinaryPolygon}. *

* * Processing Steps: *
    *
  1. First the input a gray scale image and a binarized version of it.
  2. *
  3. The contours of black blobs are found.
  4. *
  5. From the contours polygons are fitted and refined to pixel accuracy.
  6. *
  7. (Optional) Sub-pixel refinement of the polygon's edges and/or corners.
  8. *
* *

* The returned polygons will encompass the entire black polygon. Here is a simple example in 1D. If all pixels are * white, but pixels ranging from 5 to 10, inclusive, then the returned boundaries would be 5.0 to 11.0. This * means that coordinates 5.0 ≤ x < 11.0 are all black. 11.0 is included, but note that the entire pixel 11 is white. *

* * @author Peter Abeles */ public class BinaryPolygonDetector { // minimum size of a shape's contour as a fraction of the image width private double minContourFraction; private int minimumContour; // this is image.width*minContourFraction private double minimumArea; // computed from minimumContour // does the polygon have to be convex private boolean convex; private LinearContourLabelChang2004 contourFinder = new LinearContourLabelChang2004(ConnectRule.FOUR); private ImageSInt32 labeled = new ImageSInt32(1,1); // finds the initial polygon around a target candidate private SplitMergeLineFitLoop fitPolygon; // removes extra corners GrowQueue_I32 pruned = new GrowQueue_I32(); MinimizeEnergyPrune pruner; // Improve the selection of corner pixels in the contour private RefinePolyLineCorner improveContour = new RefinePolyLineCorner(true,20); // Refines the estimate of the polygon's lines using a subpixel technique private RefineBinaryPolygon refinePolygon; // List of all squares that it finds private FastQueue found = new FastQueue(Polygon2D_F64.class,true); // extera information for found shapes private FastQueue foundInfo = new FastQueue(Info.class,true); // type of input image private Class inputType; // number of lines allowed in the polygon private int minSides,maxSides; // true if points touching the border are NOT pruned private boolean canTouchBorder; // work space for initial polygon private Polygon2D_F64 workPoly = new Polygon2D_F64(); // should the order of the polygon be on clockwise order on output? private boolean outputClockwise; // storage for the contours associated with a found target. used for debugging // All contours are in distorted pixel coordiantes private List foundContours = new ArrayList(); // transforms which can be used to handle lens distortion protected PixelTransform_F32 distToUndist, undistToDist; boolean verbose = false; // used to remove false positives PolygonEdgeIntensity edgeIntensity; double edgeThreshold; // should it check the edge score before? With a chessboard pattern the initial guess is known to be very poor // so it should only check the edge after. Otherwise its good to filter before optimization. boolean checkEdgeBefore = true; // helper used to customize low level behaviors internally private PolygonHelper helper; // storage for contour in undistorted image pixels private FastQueue contourUndist = new FastQueue(Point2D_I32.class,true); /** * Configures the detector. * @param minSides minimum number of sides * @param maxSides maximum number of sides * @param contourToPolygon Fits a crude polygon to the shape's binary contour * @param refinePolygon (Optional) Refines the polygon's lines. Set to null to skip step * @param minContourFraction Size of minimum contour as a fraction of the input image's width. Try 0.23 * @param outputClockwise If true then the order of the output polygons will be in clockwise order * @param convex If true it will only return convex shapes * @param touchBorder if true then shapes which touch the image border are allowed * @param splitPenalty Penalty given to a line segment while splitting. See {@link MinimizeEnergyPrune} * @param edgeThreshold Polygons with an edge intensity less than this are discarded. * @param inputType Type of input image it's processing */ public BinaryPolygonDetector(int minSides, int maxSides, SplitMergeLineFitLoop contourToPolygon, RefineBinaryPolygon refinePolygon, double minContourFraction, boolean outputClockwise, boolean convex, boolean touchBorder, double splitPenalty, double edgeThreshold, Class inputType) { setNumberOfSides(minSides,maxSides); this.refinePolygon = refinePolygon; this.edgeIntensity = new PolygonEdgeIntensity(1,1.5,15,inputType); this.inputType = inputType; this.minContourFraction = minContourFraction; this.fitPolygon = contourToPolygon; this.outputClockwise = outputClockwise; this.convex = convex; this.canTouchBorder = touchBorder; this.edgeThreshold = edgeThreshold; pruner = new MinimizeEnergyPrune(splitPenalty); workPoly = new Polygon2D_F64(1); } /** *

Specifies transforms which can be used to change coordinates from distorted to undistorted and the opposite * coordinates. The undistorted image is never explicitly created.

* *

* WARNING: The undistorted image must have the same bounds as the distorted input image. This is because * several of the bounds checks use the image shape. This are simplified greatly by this assumption. *

* * @param width Input image width. Used in sanity check only. * @param height Input image height. Used in sanity check only. * @param distToUndist Transform from distorted to undistorted image. * @param undistToDist Transform from undistorted to distorted image. */ public void setLensDistortion( int width , int height , PixelTransform_F32 distToUndist , PixelTransform_F32 undistToDist ) { this.distToUndist = distToUndist; this.undistToDist = undistToDist; // sanity check since I think many people will screw this up. RectangleLength2D_F32 rect = DistortImageOps.boundBox_F32(width, height, distToUndist); float x1 = rect.x0 + rect.width; float y1 = rect.y0 + rect.height; float tol = 1e-4f; if( rect.getX() < -tol || rect.getY() < -tol || x1 > width+tol || y1 > height+tol ) { throw new IllegalArgumentException("You failed the idiot test! RTFM! The undistorted image "+ "must be contained by the same bounds as the input distorted image"); } if( refinePolygon != null ) { refinePolygon.setLensDistortion(width, height, distToUndist, undistToDist); } edgeIntensity.setTransform(undistToDist); } /** * Examines the undistorted gray scake input image for squares. * * @param gray Input image */ public void process(T gray, ImageUInt8 binary) { if( verbose ) System.out.println("ENTER BinaryPolygonDetector.process()"); InputSanityCheck.checkSameShape(binary, gray); if( labeled.width != gray.width || labeled.height == gray.width ) configure(gray.width,gray.height); found.reset(); foundContours.clear(); foundInfo.reset(); edgeIntensity.setImage(gray); findCandidateShapes(gray, binary); if( verbose ) System.out.println("EXIT BinaryPolygonDetector.process()"); } /** * Specifies the image's intrinsic parameters and target size * * @param width Width of the input image * @param height Height of the input image */ private void configure( int width , int height ) { // resize storage images labeled.reshape(width, height); // adjust size based parameters based on image size this.minimumContour = (int)(width*minContourFraction); this.minimumArea = Math.pow(this.minimumContour /4.0,2); if( helper != null ) helper.setImageShape(width,height); } /** * Finds blobs in the binary image. Then looks for blobs that meet size and shape requirements. See code * below for the requirements. Those that remain are considered to be target candidates. */ private void findCandidateShapes( T gray , ImageUInt8 binary ) { int maxSidesConsider = (int)Math.ceil(maxSides*1.5); // stop fitting the polygon if it clearly has way too many sides fitPolygon.setAbortSplits(2*maxSides); // find binary blobs contourFinder.process(binary, labeled); // find blobs where all 4 edges are lines FastQueue blobs = contourFinder.getContours(); for (int i = 0; i < blobs.size; i++) { Contour c = blobs.get(i); if( c.external.size() >= minimumContour) { // System.out.println("----- candidate "+c.external.size()); // ignore shapes which touch the image border boolean touchesBorder = touchesBorder(c.external); if( !canTouchBorder && touchesBorder ) { if( verbose ) System.out.println("rejected polygon, touched border"); continue; } if( helper != null ) if( !helper.filterContour(c.external,touchesBorder,true) ) continue; // remove lens distortion List contourUndist; if( distToUndist != null ) { removeDistortionFromContour(c.external,this.contourUndist); contourUndist = this.contourUndist.toList(); if( helper != null ) if( !helper.filterContour(contourUndist,touchesBorder,false) ) continue; } else { contourUndist = c.external; } if( !fitPolygon.process(contourUndist) ) { if( verbose ) System.out.println("rejected polygon initial fit failed. contour size = "+c.external.size()); continue; } GrowQueue_I32 splits = fitPolygon.getSplits(); if( splits.size() > maxSidesConsider ) { if( verbose ) System.out.println("Way too many corners, "+splits.size()+". Aborting before improve. Contour size "+c.external.size()); continue; } // Perform a local search and improve the corner placements if( !improveContour.fit(contourUndist,splits) ) { if( verbose ) System.out.println("rejected improve contour. contour size = "+c.external.size()); continue; } // reduce the number of corners based on an energy model pruner.prune(c.external, splits, pruned); splits = pruned; // only accept polygons with the expected number of sides if (!expectedNumberOfSides(splits)) { // System.out.println("First point "+c.external.get(0)); if( verbose ) System.out.println("rejected number of sides. "+splits.size()+" contour "+c.external.size()); continue; } if( helper != null ) { if( !helper.filterPixelPolygon(contourUndist,c.external,splits,touchesBorder) ) { if( verbose ) System.out.println("rejected by helper.filterPixelPolygon()"); continue; } } // convert the format of the initial crude polygon workPoly.vertexes.resize(splits.size()); for (int j = 0; j < splits.size(); j++) { Point2D_I32 p = contourUndist.get( splits.get(j)); workPoly.get(j).set(p.x,p.y); } if( helper != null ) { helper.adjustBeforeOptimize(workPoly); } // Filter out polygons which are not convex if requested by the user if( convex && !UtilPolygons2D_F64.isConvex(workPoly)) { if( verbose ) System.out.println("Rejected not convex"); continue; } // make sure it's big enough double area = Area2D_F64.polygonSimple(workPoly); if( area < minimumArea ) { if( verbose ) System.out.println("Rejected area"); continue; } // Test the edge quality and prune before performing an expensive optimization if( checkEdgeBefore && !checkPolygonEdge(workPoly,workPoly.isCCW())) { // if( verbose ) System.out.println("Rejected edge score before"); continue; } Polygon2D_F64 refined = found.grow(); refined.vertexes.resize(splits.size); boolean success; if( refinePolygon != null ) { refinePolygon.setImage(gray); success = refinePolygon.refine(workPoly,contourUndist,splits,refined); if( verbose && !success ) System.out.println("Rejected after refinePolygon"); } else { refined.set(workPoly); success = true; } boolean refinedCCW = refined.isCCW(); // test it again with the full threshold if( !checkPolygonEdge(refined,refinedCCW)) { if( verbose ) System.out.println("Rejected edge score, after"); success = false; } // refine the polygon and add it to the found list if( success ) { if( outputClockwise == refinedCCW ) refined.flip(); // System.out.println("SUCCESS!!!\n"); c.id = found.size(); foundContours.add(c); Info info = foundInfo.grow(); info.external = true; info.borderCorners.reset(); if( touchesBorder ) { // tolerance is a little bit above 0.5.pixels due to prior rounding to integer determineCornersOnBorder(refined, info.borderCorners, 0.7f); } info.edgeInside = edgeIntensity.getAverageInside(); info.edgeOutside = edgeIntensity.getAverageOutside(); } else { found.removeTail(); } } } } /** * Check to see if corners are touching the image border * @param polygon Refined polygon * @param corners storage for corner indexes */ void determineCornersOnBorder( Polygon2D_F64 polygon , GrowQueue_B corners , float tol ) { corners.reset(); for (int i = 0; i < polygon.size(); i++) { corners.add(isUndistortedOnBorder(polygon.get(i),tol)); } } /** * Coverts the point into distorted image coordinates and then checks to see if it is on the image border * @param undistorted pixel in undistorted coordinates * @param tol Tolerance for a point being on the image border * @return true if on the border or false otherwise */ boolean isUndistortedOnBorder( Point2D_F64 undistorted , float tol ) { float x,y; if( undistToDist == null ) { x = (float)undistorted.x; y = (float)undistorted.y; } else { undistToDist.compute((int)Math.round(undistorted.x),(int)Math.round(undistorted.y)); x = undistToDist.distX; y = undistToDist.distY; } return( x <= tol || y <= tol || x+tol >= labeled.width-1 || y+tol >= labeled.height-1 ); } /** * Checks to see if the edge is intense enough or if it should be discarded as noise * @return true if the edge is strong enough */ private boolean checkPolygonEdge( Polygon2D_F64 polygon , boolean ccw ) { if(!edgeIntensity.computeEdge(polygon,ccw)) { if( verbose ) System.out.println("Can't compute polygon edge intensity"); return false; } if(!edgeIntensity.checkIntensity(true,edgeThreshold)) { if( verbose ) { double inside = edgeIntensity.getAverageInside(); double outside = edgeIntensity.getAverageOutside(); System.out.println("Rejected edge score inside: " + inside+" "+outside); } return false; } return true; } /** * True if the number of sides found matches what it is looking for */ private boolean expectedNumberOfSides(GrowQueue_I32 splits) { return splits.size() >= minSides && splits.size() <= maxSides; } /** * Removes lens distortion from the found contour */ private void removeDistortionFromContour(List distorted, FastQueue undistorted ) { undistorted.reset(); for (int j = 0; j < distorted.size(); j++) { // remove distortion Point2D_I32 p = distorted.get(j); distToUndist.compute(p.x,p.y); // round to minimize error int x = Math.round(distToUndist.distX); int y = Math.round(distToUndist.distY); // save the results undistorted.grow().set(x,y); } } /** * Checks to see if some part of the contour touches the image border. Most likely cropped */ protected final boolean touchesBorder( List contour ) { int endX = labeled.width-1; int endY = labeled.height-1; for (int j = 0; j < contour.size(); j++) { Point2D_I32 p = contour.get(j); if( p.x == 0 || p.y == 0 || p.x == endX || p.y == endY ) { return true; } } return false; } public void setHelper(PolygonHelper helper) { this.helper = helper; } public boolean isConvex() { return convex; } public void setConvex(boolean convex) { this.convex = convex; } public ImageSInt32 getLabeled() { return labeled; } public boolean isOutputClockwise() { return outputClockwise; } public FastQueue getFoundPolygons() { return found; } public List getUsedContours(){return foundContours;} public List getAllContours(){return contourFinder.getContours().toList();} public Class getInputType() { return inputType; } public void setNumberOfSides( int min , int max ) { if( min < 3 ) throw new IllegalArgumentException("The min must be >= 3"); if( max < min ) throw new IllegalArgumentException("The max must be >= the min"); this.minSides = min; this.maxSides = max; } public int getMinimumSides() { return minSides; } public int getMaximumSides() { return maxSides; } public void setVerbose(boolean verbose) { this.verbose = verbose; } public boolean isCheckEdgeBefore() { return checkEdgeBefore; } public RefineBinaryPolygon getRefinePolygon() { return refinePolygon; } public void setRefinePolygon(RefineBinaryPolygon refinePolygon) { this.refinePolygon = refinePolygon; } public double getEdgeThreshold() { return edgeThreshold; } public void setEdgeThreshold(double edgeThreshold) { this.edgeThreshold = edgeThreshold; } public PixelTransform_F32 getDistToUndist() { return distToUndist; } public PixelTransform_F32 getUndistToDist() { return undistToDist; } /** * Returns additional information on the polygon */ public FastQueue getPolygonInfo() { return foundInfo; } /** * If set to true it will prune using polygons using their edge intensity before sub-pixel optimization. * This should only be set to false if the initial edge is known to be off by a bit, like with a chessboard. * @param checkEdgeBefore true for checking before and false for after. */ public void setCheckEdgeBefore(boolean checkEdgeBefore) { this.checkEdgeBefore = checkEdgeBefore; } public static class Info { /** * Was it created from an external or internal contour */ public boolean external; /** * Average pixel intensity score along the polygon's edge inside and outside */ public double edgeInside,edgeOutside; /** * Boolean value for each corner being along the border. If empty then non of the corners are long the border. * true means the corner is a border corner. */ public GrowQueue_B borderCorners = new GrowQueue_B(); public boolean touchesBorder() { if( borderCorners.size() == 0 ) return false; else { for (int i = 0; i < borderCorners.size(); i++) { if( borderCorners.get(i)) return true; } } return false; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy