/* * 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.abst.distort.FDistort; import boofcv.abst.filter.binary.InputToBinary; import boofcv.alg.distort.PixelTransformAffine_F32; import boofcv.alg.misc.GImageMiscOps; import boofcv.alg.misc.ImageMiscOps; import boofcv.core.image.GeneralizedImageOps; import boofcv.factory.filter.binary.FactoryThresholdBinary; import boofcv.factory.shape.ConfigPolygonDetector; import boofcv.factory.shape.ConfigRefinePolygonCornersToImage; import boofcv.factory.shape.ConfigRefinePolygonLineToImage; import boofcv.factory.shape.FactoryShapeDetector; import boofcv.gui.image.ShowImages; import boofcv.io.image.ConvertBufferedImage; import boofcv.struct.distort.PixelTransform2_F32; import boofcv.struct.image.GrayF32; import boofcv.struct.image.GrayU8; import boofcv.struct.image.ImageGray; import georegression.geometry.UtilPolygons2D_F64; import georegression.struct.affine.Affine2D_F32; import georegression.struct.affine.Affine2D_F64; import georegression.struct.affine.UtilAffine; import georegression.struct.point.Point2D_F64; import georegression.struct.point.Point2D_I32; import georegression.struct.shapes.Polygon2D_F64; import georegression.struct.shapes.Rectangle2D_F64; import georegression.struct.shapes.Rectangle2D_I32; import georegression.transform.affine.AffinePointOps_F64; import org.ddogleg.struct.FastQueue; import org.ddogleg.struct.GrowQueue_B; import org.junit.Before; import org.junit.Test; import java.awt.*; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; import static org.junit.Assert.*; /** * @author Peter Abeles */ public class TestBinaryPolygonDetector { int width = 400,height=450; boolean showRendered = false; GrayU8 binary = new GrayU8(1,1); ImageGray orig; ImageGray dist; Class imageTypes[] = new Class[]{GrayU8.class, GrayF32.class}; List<Rectangle2D_I32> rectangles = new ArrayList<>(); List<Polygon2D_F64> distorted = new ArrayList<>(); Affine2D_F64 transform = new Affine2D_F64(); InputToBinary<GrayU8> inputToBinary_U8 = FactoryThresholdBinary.globalFixed(100, true, GrayU8.class); @Before public void before() { rectangles.clear(); transform.reset(); } /** * See if it uses the provided lens distortion transforms correctly. The distortion applied * is actually the affine transform instead of lens distortion. It should find the original * rectangles. */ @Test public void usingSetLensDistortion() { rectangles.add(new Rectangle2D_I32(30,30,60,60)); rectangles.add(new Rectangle2D_I32(90,30,120,60)); rectangles.add(new Rectangle2D_I32(30,90,60,120)); rectangles.add(new Rectangle2D_I32(90,90,120,120)); transform.set(0.8, 0, 0, 0.8, 1, 2); transform = transform.invert(null); for( Class imageType : imageTypes ) { checkDetected_LensDistortion(imageType, true, 0.5); checkDetected_LensDistortion(imageType, false, 0.5); } } private void checkDetected_LensDistortion(Class imageType, boolean useLines , double tol) { renderDistortedRectangle(imageType); Affine2D_F32 a = new Affine2D_F32(); UtilAffine.convert(transform,a); PixelTransform2_F32 tranFrom = new PixelTransformAffine_F32(a); PixelTransform2_F32 tranTo = new PixelTransformAffine_F32(a.invert(null)); int numberOfSides = 4; BinaryPolygonDetector alg = createDetector(imageType, useLines, numberOfSides,numberOfSides); alg.setLensDistortion(dist.width, dist.height, tranTo, tranFrom); alg.process(dist, binary); FastQueue<Polygon2D_F64> found = alg.getFoundPolygons(); assertEquals(rectangles.size(),found.size); for (int i = 0; i < found.size; i++) { assertEquals(1, findMatchesOriginal(found.get(i), tol)); } } @Test public void easyTestNoDistortion() { rectangles.add(new Rectangle2D_I32(30,30,60,60)); rectangles.add(new Rectangle2D_I32(90,30,120,60)); rectangles.add(new Rectangle2D_I32(30,90,60,120)); rectangles.add(new Rectangle2D_I32(90,90,120,120)); for( Class imageType : imageTypes ) { checkDetected(imageType,true,1e-8); checkDetected(imageType,false,1e-8); } } @Test public void someAffineDistortion() { rectangles.add(new Rectangle2D_I32(30,30,60,60)); rectangles.add(new Rectangle2D_I32(90,30,120,60)); rectangles.add(new Rectangle2D_I32(30,90,60,120)); rectangles.add(new Rectangle2D_I32(90,90,120,120)); transform.set(1.1, 0.2, 0.12, 1.3, 10.2, 20.3); for( Class imageType : imageTypes ) { checkDetected(imageType,true,0.3); checkDetected(imageType,false,0.5); } } private void checkDetected(Class imageType, boolean useLines, double tol ) { renderDistortedRectangle(imageType); int numberOfSides = 4; BinaryPolygonDetector alg = createDetector(imageType, useLines, numberOfSides,numberOfSides); alg.process(dist, binary); FastQueue<Polygon2D_F64> found = alg.getFoundPolygons(); assertEquals(rectangles.size(), found.size); for (int i = 0; i < found.size; i++) { assertEquals(1,findMatches(found.get(i),tol)); } } @Test public void easyTestMultipleShapes() { List<Polygon2D_F64> polygons = new ArrayList<>(); polygons.add(new Polygon2D_F64(20, 20, 40, 50, 80, 20)); polygons.add(new Polygon2D_F64(20, 60, 20, 90, 40, 90,40, 60)); for( Class imageType : imageTypes ) { checkDetectedMulti(imageType, polygons, true,1.5); checkDetectedMulti(imageType, polygons, false,2); } } private void checkDetectedMulti(Class imageType,List<Polygon2D_F64> polygons, boolean useLines, double tol ) { renderPolygons(polygons,imageType); BinaryPolygonDetector alg = createDetector(imageType, useLines, 3,4); alg.process(dist, binary); FastQueue<Polygon2D_F64> found = alg.getFoundPolygons(); assertEquals(polygons.size(), found.size); for (int i = 0; i < found.size; i++) { assertEquals(1,findMatches(found.get(i),tol)); } } private <T extends ImageGray> BinaryPolygonDetector<T> createDetector(Class<T> imageType, boolean useLines, int minSides, int maxSides) { ConfigPolygonDetector config = new ConfigPolygonDetector(minSides,maxSides); if( useLines ) { config.refine = new ConfigRefinePolygonLineToImage(); } else { config.refine = new ConfigRefinePolygonCornersToImage(); } return FactoryShapeDetector.polygon(config,imageType); } /** * Compare found rectangle against rectangles in the original undistorted image */ private int findMatchesOriginal(Polygon2D_F64 found, double tol) { int match = 0; for (int i = 0; i < rectangles.size(); i++) { Rectangle2D_I32 ri = rectangles.get(i); Rectangle2D_F64 r = new Rectangle2D_F64(ri.x0,ri.y0,ri.x1,ri.y1); Polygon2D_F64 p = new Polygon2D_F64(4); UtilPolygons2D_F64.convert(r,p); if( p.isCCW() ) p.flip(); if(UtilPolygons2D_F64.isEquivalent(found,p,tol)) match++; } return match; } private int findMatches( Polygon2D_F64 found , double tol ) { int match = 0; for (int i = 0; i < distorted.size(); i++) { if(UtilPolygons2D_F64.isEquivalent(found, distorted.get(i),tol)) match++; } return match; } public void renderPolygons( List<Polygon2D_F64> polygons, Class imageType ) { InputToBinary inputToBinary = FactoryThresholdBinary.globalFixed(100, true, imageType); BufferedImage work = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = work.createGraphics(); g2.setColor(Color.WHITE); g2.fillRect(0, 0, width, height); g2.setColor(Color.BLACK); distorted.clear(); for (int i = 0; i < polygons.size(); i++) { Polygon2D_F64 orig = polygons.get(i); int x[] = new int[ orig.size() ]; int y[] = new int[ orig.size() ]; for (int j = 0; j < orig.size(); j++) { x[j] = (int)orig.get(j).x; y[j] = (int)orig.get(j).y; } g2.fillPolygon(x,y,orig.size()); distorted.add( orig ); } dist = GeneralizedImageOps.createSingleBand(imageType, width, height); binary = new GrayU8(width,height); ConvertBufferedImage.convertFrom(work,dist,true); inputToBinary.process(dist,binary); if( showRendered ) { ShowImages.showWindow(work, "Rendered"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void renderDistortedRectangle( Class imageType ) { InputToBinary inputToBinary = FactoryThresholdBinary.globalFixed(100, true, imageType); orig = GeneralizedImageOps.createSingleBand(imageType,width,height); dist = GeneralizedImageOps.createSingleBand(imageType,width,height); binary.reshape(width,height); GImageMiscOps.fill(orig, 200); GImageMiscOps.fill(dist, 200); distorted.clear(); for (Rectangle2D_I32 q : rectangles) { GImageMiscOps.fillRectangle(orig,10,q.x0,q.y0,q.x1-q.x0,q.y1-q.y0); Polygon2D_F64 tran = new Polygon2D_F64(4); AffinePointOps_F64.transform(transform,q.x0,q.y0,tran.get(0)); AffinePointOps_F64.transform(transform,q.x0,q.y1,tran.get(1)); AffinePointOps_F64.transform(transform,q.x1,q.y1,tran.get(2)); AffinePointOps_F64.transform(transform,q.x1,q.y0,tran.get(3)); distorted.add(tran); } new FDistort(orig,dist).border(200).affine(transform).apply(); inputToBinary.process(dist,binary); if( showRendered ) { BufferedImage out = ConvertBufferedImage.convertTo(dist, null, true); ShowImages.showWindow(out, "Rendered"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } } } @Test public void rejectShapes_circle() { BufferedImage work = new BufferedImage(200,220,BufferedImage.TYPE_INT_RGB); Graphics2D g2 = work.createGraphics(); g2.setColor(Color.WHITE); g2.fillRect(0,0,200,220); g2.setColor(Color.BLACK); g2.fillOval(30, 30, 90, 100); GrayU8 gray = ConvertBufferedImage.convertFrom(work,(GrayU8)null); binary.reshape(gray.width,gray.height); inputToBinary_U8.process(gray,binary); for (int i = 3; i <= 6; i++) { BinaryPolygonDetector alg = createDetector(GrayU8.class, true, i,i); alg.process(gray,binary); assertEquals("num sides = "+i,0,alg.getFoundPolygons().size()); } } @Test public void rejectShapes_triangle() { BufferedImage work = new BufferedImage(200,220,BufferedImage.TYPE_INT_RGB); Graphics2D g2 = work.createGraphics(); g2.setColor(Color.WHITE); g2.fillRect(0,0,200,220); g2.setColor(Color.BLACK); g2.fillPolygon(new int[]{10, 50, 30}, new int[]{10, 10, 40}, 3); GrayU8 gray = ConvertBufferedImage.convertFrom(work,(GrayU8)null); binary.reshape(gray.width,gray.height); inputToBinary_U8.process(gray,binary); for (int i = 3; i <= 6; i++) { BinaryPolygonDetector alg = createDetector(GrayU8.class, true, i, i); alg.process(gray,binary); if( i == 3 ) { double tol = 0.5; assertEquals(1, alg.getFoundPolygons().size()); Polygon2D_F64 found = (Polygon2D_F64)alg.getFoundPolygons().get(0); checkPolygon(new double[]{10, 10, 30, 40, 50, 10}, found); } else assertEquals(0,alg.getFoundPolygons().size()); } } public static boolean checkPolygon( double[] expected , Polygon2D_F64 found ) { for (int i = 0; i < found.size(); i++) { boolean matched = true; for (int j = 0; j < found.size(); j++) { double x = expected[j*2]; double y = expected[j*2+1]; Point2D_F64 p = found.get((i+j)%found.size()); if( Math.abs(p.x-x) > 1e-5 || Math.abs(p.y-y) > 1e-5 ) { matched = false; break; } } if( matched ) return true; } return false; } /** * Configure the detector to reject concave shapes */ @Test public void rejectShapes_concave() { List<Polygon2D_F64> polygons = new ArrayList<>(); polygons.add(new Polygon2D_F64(20,20, 80,20, 80,80, 40,40, 20,80)); for( Class imageType : imageTypes ) { renderPolygons(polygons, imageType); BinaryPolygonDetector alg = createDetector(imageType, true, 5,5); alg.process(dist,binary); assertEquals(0,alg.getFoundPolygons().size()); } } /** * Give it an easy to detect concave shape */ @Test public void detect_concave() { List<Polygon2D_F64> polygons = new ArrayList<>(); Polygon2D_F64 expected = new Polygon2D_F64(20, 20, 20, 80, 40, 40, 80, 80, 80, 20); polygons.add(expected); for( Class imageType : imageTypes ) { renderPolygons(polygons,imageType ); BinaryPolygonDetector alg = createDetector(imageType, true, 5,5); alg.setConvex(false); alg.process(dist, binary); assertEquals(1, alg.getFoundPolygons().size()); Polygon2D_F64 found = (Polygon2D_F64)alg.getFoundPolygons().get(0); assertEquals(1, findMatches(found, 1)); } } @Test public void touchesBorder_false() { List<Point2D_I32> contour = new ArrayList<>(); BinaryPolygonDetector alg = createDetector(GrayU8.class, true, 4,4); alg.getLabeled().reshape(20,30); assertFalse(alg.touchesBorder(contour)); contour.add(new Point2D_I32(10,1)); assertFalse(alg.touchesBorder(contour)); contour.add(new Point2D_I32(10,28)); assertFalse(alg.touchesBorder(contour)); contour.add(new Point2D_I32(1,15)); assertFalse(alg.touchesBorder(contour)); contour.add(new Point2D_I32(18,15)); assertFalse(alg.touchesBorder(contour)); } /** * When an adaptive threshold is used, the area around a bright light gets marked as "dark" then when * the polygon is fit to it it can snap around the white object */ @Test public void snapToBrightObject() { GrayU8 gray = new GrayU8(200,200); GrayU8 binary = new GrayU8(200,200); ImageMiscOps.fillRectangle(gray,200,40,40,40,40); ImageMiscOps.fillRectangle(binary,1,38,38,44,44); ImageMiscOps.fillRectangle(binary,0,40,40,40,40); BinaryPolygonDetector<GrayU8> alg = createDetector(GrayU8.class, true, 4,4); // edge threshold test will only fail now if the sign is reversed alg.edgeThreshold = 0; alg.process(gray,binary); assertEquals(0,alg.getFoundPolygons().size); } @Test public void determineCornersOnBorder() { BinaryPolygonDetector alg = createDetector(GrayU8.class, true, 4,4); alg.getLabeled().reshape(width,height); Polygon2D_F64 poly = new Polygon2D_F64(0,0, 10,0, 10,10, 0,10); GrowQueue_B corners = new GrowQueue_B(); alg.determineCornersOnBorder(poly,corners,0.5f); assertEquals(4,corners.size()); assertEquals(true,corners.get(0)); assertEquals(true,corners.get(1)); assertEquals(false,corners.get(2)); assertEquals(true,corners.get(3)); } @Test public void isUndistortedOnBorder() { Affine2D_F32 a = new Affine2D_F32(); transform.set(1.2,0,0,1.2,0,0); UtilAffine.convert(transform,a); PixelTransform2_F32 tranFrom = new PixelTransformAffine_F32(a); PixelTransform2_F32 tranTo = new PixelTransformAffine_F32(a.invert(null)); BinaryPolygonDetector alg = createDetector(GrayU8.class, true, 4,4); alg.undistToDist = tranFrom; alg.distToUndist = tranTo; alg.getLabeled().reshape(width,height); List<Point2D_I32> positive = new ArrayList<>(); positive.add( new Point2D_I32(20,0)); positive.add( new Point2D_I32(width-1,30)); positive.add( new Point2D_I32(0,30)); positive.add( new Point2D_I32(20,height-1)); for( Point2D_I32 p : positive ) { alg.distToUndist.compute(p.x,p.y); float x = alg.distToUndist.distX; float y = alg.distToUndist.distY; assertTrue(alg.isUndistortedOnBorder(new Point2D_F64(x,y),0.7f)); } List<Point2D_I32> negative = new ArrayList<>(); negative.add( new Point2D_I32(20,5)); negative.add( new Point2D_I32(width-3,30)); negative.add( new Point2D_I32(7,30)); negative.add( new Point2D_I32(20,height-4)); for( Point2D_I32 p : negative ) { alg.distToUndist.compute(p.x,p.y); float x = alg.distToUndist.distX; float y = alg.distToUndist.distY; assertFalse(alg.isUndistortedOnBorder(new Point2D_F64(x,y),0.7f)); } } }