/* * 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.fiducial.calib.circle; import boofcv.abst.filter.binary.InputToBinary; import boofcv.alg.fiducial.calib.circle.EllipseClustersIntoAsymmetricGrid.Grid; import boofcv.alg.shapes.ellipse.BinaryEllipseDetector; import boofcv.factory.filter.binary.FactoryThresholdBinary; import boofcv.factory.shape.FactoryShapeDetector; import boofcv.io.image.ConvertBufferedImage; import boofcv.struct.image.GrayU8; import georegression.geometry.GeometryMath_F64; import georegression.struct.affine.Affine2D_F64; import georegression.struct.point.Point2D_F64; import georegression.struct.point.Vector3D_F64; import georegression.struct.shapes.EllipseRotated_F64; import org.ddogleg.struct.FastQueue; 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.assertEquals; import static org.junit.Assert.assertTrue; /** * @author Peter Abeles */ public class TestDetectAsymmetricCircleGrid { @Test public void process_easy() { Affine2D_F64 affine = new Affine2D_F64(1,0,0,1,100,100); performDetectionCheck(5, 6, 5, 6, affine); performDetectionCheck(5, 4, 5, 4, affine); performDetectionCheck(3, 3, 3, 3, affine); performDetectionCheck(3, 4, 3, 4, affine); performDetectionCheck(4, 3, 4, 3, affine); } @Test public void process_rotated() { double c = Math.cos(0.4); double s = Math.sin(0.4); Affine2D_F64 affine = new Affine2D_F64(c,-s,s,c,100,100); performDetectionCheck(5, 6, 5, 6, affine); } @Test public void process_negative() { Affine2D_F64 affine = new Affine2D_F64(1,0,0,1,100,100); performDetectionCheck(4, 6, 5, 6, affine); } private void performDetectionCheck(int expectedRows, int expectedCols, int actualRows, int actualCols, Affine2D_F64 affine) { int radius = 20; int centerDistances = 80; DetectAsymmetricCircleGrid<GrayU8> alg = createAlg(expectedRows,expectedCols,radius,centerDistances); // alg.setVerbose(true); List<Point2D_F64> locations = new ArrayList<>(); GrayU8 image = new GrayU8(400,450); render(actualRows,actualCols, radius, centerDistances, affine, locations,image); alg.process(image); List<Grid> found = alg.getGrids(); if( expectedRows != actualRows || expectedCols != actualCols ) { assertEquals(0, found.size()); return; } else { assertEquals(1, found.size()); } Grid g = found.get(0); assertEquals(actualRows , g.rows ); assertEquals(actualCols , g.columns ); checkCounterClockWise(g); int index = 0; for (int row = 0; row < g.rows; row++) { for (int col = 0; col < g.columns; col++) { boolean check = false; if( row%2 == 1 && col%2 == 1) check = true; else if( row%2 == 0 && col%2 == 0) check = true; if( check ) { EllipseRotated_F64 f = g.get(row,col); Point2D_F64 e = locations.get(index++); assertEquals( e.x , f.center.x , 1.5 ); assertEquals( e.y , f.center.y , 1.5 ); assertEquals( 20 , f.a , 1.0 ); assertEquals( 20 , f.b , 1.0 ); } } } } private DetectAsymmetricCircleGrid<GrayU8> createAlg( int numRows , int numCols , double radius , double centerDistance ) { double spaceRatio = 1.2*centerDistance/radius; InputToBinary<GrayU8> threshold = FactoryThresholdBinary.globalFixed(100,true,GrayU8.class); BinaryEllipseDetector<GrayU8> detector = FactoryShapeDetector.ellipse(null, GrayU8.class); EllipsesIntoClusters cluster = new EllipsesIntoClusters(spaceRatio,0.8); return new DetectAsymmetricCircleGrid<>( numRows, numCols,threshold, detector, cluster); } @Test public void pruneIncorrectSize() { List<List<EllipsesIntoClusters.Node>> clusters = new ArrayList<>(); clusters.add( createListNodes(4)); clusters.add( createListNodes(10)); clusters.add( createListNodes(11)); DetectAsymmetricCircleGrid.pruneIncorrectSize(clusters, 10); assertEquals(1,clusters.size()); assertEquals(10,clusters.get(0).size()); } private static List<EllipsesIntoClusters.Node> createListNodes( int N ) { List<EllipsesIntoClusters.Node> list = new ArrayList<>(); for (int i = 0; i < N; i++) { list.add( new EllipsesIntoClusters.Node() ); } return list; } @Test public void pruneIncorrectShape() { FastQueue<Grid> grids = new FastQueue<>(Grid.class,true); grids.grow().setShape(4,5); grids.grow().setShape(5,4); grids.grow().setShape(4,3); grids.grow().setShape(5,5); DetectAsymmetricCircleGrid.pruneIncorrectShape(grids, 4, 5); assertEquals( 2 , grids.size ); } /** * Vertical flip is needed to put it into the correct order */ @Test public void putGridIntoCanonical_vertical() { putGridIntoCanonical_vertical(5,2); putGridIntoCanonical_vertical(5,6); } private void putGridIntoCanonical_vertical( int numRows , int numCols ) { DetectAsymmetricCircleGrid<?> alg = new DetectAsymmetricCircleGrid<>(numRows,numCols,null,null,null); Grid g = createGrid(numRows,numCols); List<EllipseRotated_F64> original = new ArrayList<>(); original.addAll(g.ellipses); alg.putGridIntoCanonical(g); assertEquals(numRows,g.rows); assertEquals(numCols,g.columns); assertTrue(original.get(0) == g.get(0,0)); checkCounterClockWise(g); alg.putGridIntoCanonical(flipVertical(g)); assertEquals(numRows,g.rows); assertEquals(numCols,g.columns); assertTrue(original.get(0) == g.get(0,0)); checkCounterClockWise(g); } /** * Horizontal flip is needed to put it into the correct order */ @Test public void putGridIntoCanonical_horizontal() { putGridIntoCanonical_horizontal(2,5); putGridIntoCanonical_horizontal(6,5); } private void putGridIntoCanonical_horizontal( int numRows , int numCols) { DetectAsymmetricCircleGrid<?> alg = new DetectAsymmetricCircleGrid<>(numRows,numCols,null,null,null); Grid g = createGrid(numRows,numCols); List<EllipseRotated_F64> original = new ArrayList<>(); original.addAll(g.ellipses); alg.putGridIntoCanonical(g); assertEquals(numRows,g.rows); assertEquals(numCols,g.columns); assertTrue(original.get(0) == g.get(0,0)); checkCounterClockWise(g); g = flipHorizontal(g); alg.putGridIntoCanonical(g); assertEquals(numRows,g.rows); assertEquals(numCols,g.columns); assertTrue(original.get(0) == g.get(0,0)); checkCounterClockWise(g); } /** * Horizontal flip is needed to put it into the correct order */ @Test public void putGridIntoCanonical_rotate() { putGridIntoCanonical_rotate(3,3); putGridIntoCanonical_rotate(5,3); } public void putGridIntoCanonical_rotate(int numRows , int numCols ) { DetectAsymmetricCircleGrid<?> alg = new DetectAsymmetricCircleGrid<>(numRows,numCols,null,null,null); Grid g = createGrid(numRows,numCols); List<EllipseRotated_F64> original = new ArrayList<>(); original.addAll(g.ellipses); alg.putGridIntoCanonical(g); assertEquals(numRows,g.rows); assertEquals(numCols,g.columns); assertTrue(original.get(0) == g.get(0,0)); checkCounterClockWise(g); alg.rotateGridCCW(g); alg.putGridIntoCanonical(g); assertTrue(original.get(0) == g.get(0,0)); checkCounterClockWise(g); alg.rotateGridCCW(g); alg.rotateGridCCW(g); alg.putGridIntoCanonical(g); assertTrue(original.get(0) == g.get(0,0)); checkCounterClockWise(g); alg.rotateGridCCW(g); alg.rotateGridCCW(g); alg.rotateGridCCW(g); alg.putGridIntoCanonical(g); assertTrue(original.get(0) == g.get(0,0)); checkCounterClockWise(g); alg.flipVertical(g); alg.putGridIntoCanonical(g); assertTrue(original.get(0) == g.get(0,0)); checkCounterClockWise(g); } private void checkCounterClockWise(Grid g ) { EllipseRotated_F64 a = g.get(0,0); EllipseRotated_F64 b = g.columns>=3 ? g.get(0,2) : g.get(1,1); EllipseRotated_F64 c = g.rows>=3 ? g.get(2,0) : g.get(1,1); double dx0 = b.center.x - a.center.x; double dy0 = b.center.y - a.center.y; double dx1 = c.center.x - a.center.x; double dy1 = c.center.y - a.center.y; Vector3D_F64 v = new Vector3D_F64(); GeometryMath_F64.cross(dx0,dy0,0, dx1,dy1,0, v); assertTrue(v.z>0); } @Test public void closestCorner4() { Grid g = new Grid(); g.rows = 3; g.columns = 3; g.ellipses.add(new EllipseRotated_F64(20,20, 0,0,0)); g.ellipses.add(null); g.ellipses.add(new EllipseRotated_F64(20,100, 0,0,0)); g.ellipses.add(null); g.ellipses.add(new EllipseRotated_F64()); g.ellipses.add(null); g.ellipses.add(new EllipseRotated_F64(100,20, 0,0,0)); g.ellipses.add(null); g.ellipses.add(new EllipseRotated_F64(100,100, 0,0,0)); assertEquals(0, DetectAsymmetricCircleGrid.closestCorner4(g)); g.ellipses.get(0).center.set(20,100); g.ellipses.get(2).center.set(100,20); g.ellipses.get(6).center.set(100,100); g.ellipses.get(8).center.set(20,20); assertEquals(2, DetectAsymmetricCircleGrid.closestCorner4(g)); g.ellipses.get(0).center.set(100,20); g.ellipses.get(2).center.set(100,100); g.ellipses.get(6).center.set(20,20); g.ellipses.get(8).center.set(20,100); assertEquals(1, DetectAsymmetricCircleGrid.closestCorner4(g)); g.ellipses.get(0).center.set(100,100); g.ellipses.get(2).center.set(20,20); g.ellipses.get(6).center.set(20,100); g.ellipses.get(8).center.set(100,20); assertEquals(3, DetectAsymmetricCircleGrid.closestCorner4(g)); } private Grid createGrid(int numRows, int numCols) { Grid g = new Grid(); g.rows = numRows; g.columns = numCols; for (int i = 0; i < numRows; i++) { for (int j = 0; j < numCols; j++) { if( i%2 == 0 ) { if( j%2 == 0 ) g.ellipses.add(new EllipseRotated_F64(j*20,i*20, 0,0,0)); else g.ellipses.add(null); } else { if( j%2 == 0 ) g.ellipses.add(null); else g.ellipses.add(new EllipseRotated_F64(j*20,i*20, 0,0,0)); } } } return g; } @Test public void rotateGridCCW() { Grid g = createGrid(3,3); List<EllipseRotated_F64> original = new ArrayList<>(); original.addAll(g.ellipses); DetectAsymmetricCircleGrid<?> alg = new DetectAsymmetricCircleGrid<>(3,3,null,null,null); alg.rotateGridCCW(g); assertEquals(9,g.ellipses.size()); assertTrue( original.get(6) == g.get(0,0)); assertTrue( original.get(0) == g.get(0,2)); assertTrue( original.get(2) == g.get(2,2)); assertTrue( original.get(8) == g.get(2,0)); } private Grid flipHorizontal( Grid g ) { Grid out = new Grid(); for (int i = 0; i < g.rows; i++) { for (int j = 0; j < g.columns; j++) { out.ellipses.add( g.get(i,g.columns-j-1) ); } } out.columns = g.columns; out.rows = g.rows; return out; } private Grid flipVertical( Grid g ) { Grid out = new Grid(); for (int i = 0; i < g.rows; i++) { for (int j = 0; j < g.columns; j++) { out.ellipses.add( g.get(g.rows-i-1,j) ); } } out.columns = g.columns; out.rows = g.rows; return out; } public static void render(int rows , int cols , double radius , double centerDistance, Affine2D_F64 affine, List<Point2D_F64> locations , GrayU8 image ) { BufferedImage buffered = new BufferedImage(image.width,image.height, BufferedImage.TYPE_INT_BGR); Graphics2D g2 = buffered.createGraphics(); g2.setColor(Color.WHITE); g2.fillRect(0,0,buffered.getWidth(),buffered.getHeight()); g2.setColor(Color.BLACK); locations.clear(); for (int row = 0; row < rows; row++) { double y = row*centerDistance/2.0; for (int col = 0; col < cols; col++) { double x = col*centerDistance/2.0; if( row%2 == 1 && col%2 ==0 ) continue; if( row%2 == 0 && col%2 ==1 ) continue; double xx = affine.a11*x + affine.a12*y + affine.tx; double yy = affine.a21*x + affine.a22*y + affine.ty; g2.fillOval((int)(xx-radius+0.5),(int)(yy-radius+0.5),(int)(radius*2),(int)(radius*2)); locations.add( new Point2D_F64(xx,yy)); } } ConvertBufferedImage.convertFrom(buffered, image); // ShowImages.showWindow(buffered,"Rendered",true); // try { Thread.sleep(10000); } catch (InterruptedException ignore) {} } }