/* * Copyright (C) 2011-2016, Peter Abeles. All Rights Reserved. * * This file is part of Geometric Regression Library (GeoRegression). * * 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 georegression.geometry; import georegression.geometry.algs.TangentLinesTwoEllipses_F32; import georegression.metric.Intersection2D_F32; import georegression.metric.UtilAngle; import georegression.misc.GrlConstants; import georegression.struct.line.LineGeneral2D_F32; import georegression.struct.point.Point2D_F32; import georegression.struct.point.Vector2D_F32; import georegression.struct.shapes.EllipseQuadratic_F32; import georegression.struct.shapes.EllipseRotated_F32; import org.junit.Test; import java.util.Random; import static org.junit.Assert.*; /** * @author Peter Abeles */ public class TestUtilEllipse_F32 { Random rand = new Random(234); @Test public void convert_back_forth() { convert_back_forth(0,0,4.5f,3,0); convert_back_forth(1,2,4.5f,3,0); convert_back_forth(0,0,4.5f,3,(float)Math.PI/4); convert_back_forth(0,0,4.5f,3,0.1f); convert_back_forth(-2,1.5f,4.5f,3,-0.1f); convert_back_forth(1,2,3,1.5f,0); convert_back_forth(1,2,3,1.5f,1.5f); // see if it can handle a circle convert_back_forth(0,0,3,3,0); } @Test public void convert_back_forth_random() { for( int i = 0; i < 100; i++ ) { float x = (rand.nextFloat()-0.5f)*2; float y = (rand.nextFloat()-0.5f)*2; float theta = (rand.nextFloat()-0.5f)*(float)Math.PI; float b = rand.nextFloat()*2+0.1f; float a = b+rand.nextFloat(); convert_back_forth(x,y,a,b,theta); } } public void convert_back_forth( float x0 , float y0, float a, float b, float phi ) { // should be scale invariant convert_back_forth(x0,y0,a,b,phi,1); convert_back_forth(x0,y0,a,b,phi,-1); } public void convert_back_forth( float x0 , float y0, float a, float b, float phi , float scale ) { EllipseRotated_F32 rotated = new EllipseRotated_F32(x0,y0,a,b,phi); EllipseQuadratic_F32 quad = new EllipseQuadratic_F32(); EllipseRotated_F32 found = new EllipseRotated_F32(); UtilEllipse_F32.convert(rotated,quad); quad.a *= scale; quad.b *= scale; quad.c *= scale; quad.d *= scale; quad.e *= scale; quad.f *= scale; UtilEllipse_F32.convert(quad,found); assertEquals(rotated.center.x,found.center.x, GrlConstants.FLOAT_TEST_TOL); assertEquals(rotated.center.y,found.center.y, GrlConstants.FLOAT_TEST_TOL); assertEquals(rotated.a,found.a, GrlConstants.FLOAT_TEST_TOL); assertEquals(rotated.b,found.b, GrlConstants.FLOAT_TEST_TOL); assertEquals(rotated.phi,found.phi, GrlConstants.FLOAT_TEST_TOL); } @Test public void convert_rotated_to_quad() { EllipseRotated_F32 rotated = new EllipseRotated_F32(1,2,4.5f,3,0.2f); Point2D_F32 p = UtilEllipse_F32.computePoint(0.45f,rotated,null); float eval = UtilEllipse_F32.evaluate(p.x,p.y,rotated); assertEquals(1,eval, GrlConstants.FLOAT_TEST_TOL); EllipseQuadratic_F32 quad = new EllipseQuadratic_F32(); UtilEllipse_F32.convert(rotated,quad); eval = UtilEllipse_F32.evaluate(p.x,p.y,quad); assertEquals(0,eval, GrlConstants.FLOAT_TEST_TOL); } /** * Tests computePoint and evaluate(rotated) by computes points around the ellipse and seeing if they * meet the expected results. */ @Test public void computePoint_evaluate_rotated() { EllipseRotated_F32 rotated = new EllipseRotated_F32(1,2,4.5f,3,0.2f); for( int i = 0; i < 100; i++ ) { float t = (float)Math.PI*2*i/100.0f; Point2D_F32 p = UtilEllipse_F32.computePoint(t,rotated,null); float eval = UtilEllipse_F32.evaluate(p.x,p.y,rotated); assertEquals(1,eval, GrlConstants.FLOAT_TEST_TOL); } } @Test public void computePoint_evaluate_quadratic() { EllipseRotated_F32 rotated = new EllipseRotated_F32(1,2,4.5f,3,0.2f); EllipseQuadratic_F32 quad = new EllipseQuadratic_F32(); UtilEllipse_F32.convert(rotated,quad); for( int i = 0; i < 100; i++ ) { float t = GrlConstants.F_PI*2*i/100.0f; Point2D_F32 p = UtilEllipse_F32.computePoint(t,rotated,null); float eval = UtilEllipse_F32.evaluate(p.x,p.y,quad); assertEquals(0,eval, GrlConstants.FLOAT_TEST_TOL); } } /** * Try a few simple cases */ @Test public void computePoint_rotated() { EllipseRotated_F32 rotated = new EllipseRotated_F32(1,2,3,2,GrlConstants.F_PId2); Point2D_F32 p = UtilEllipse_F32.computePoint(0,rotated,null); assertEquals(1.0f,p.x, GrlConstants.FLOAT_TEST_TOL); assertEquals(2+3,p.y, GrlConstants.FLOAT_TEST_TOL); p = UtilEllipse_F32.computePoint(GrlConstants.F_PId2,rotated,null); assertEquals(-1.0f,p.x, GrlConstants.FLOAT_TEST_TOL); assertEquals( 2.0f,p.y, GrlConstants.FLOAT_TEST_TOL); } @Test public void computeAngle() { EllipseRotated_F32 rotated = new EllipseRotated_F32(1,2,4.5f,3,0.2f); for( int i = 0; i <= 100; i++ ) { float t = GrlConstants.F_PI*2*i/100.0f - GrlConstants.F_PI; Point2D_F32 p = UtilEllipse_F32.computePoint(t,rotated,null); float found = UtilEllipse_F32.computeAngle(p,rotated); // System.out.println(t+" "+found); assertTrue(UtilAngle.dist(t, found) <= GrlConstants.FLOAT_TEST_TOL); } } @Test public void computeTangent_rotated() { float delta = GrlConstants.FLOAT_TEST_TOL; // axis aligned case EllipseRotated_F32 rotated = new EllipseRotated_F32(1,2,4.5f,3,0); for (int i = 0; i < 20; i++) { float theta = i*GrlConstants.F_PI*2.0f/20.0f; checkTangent(theta,rotated,delta); } // rotated case rotated = new EllipseRotated_F32(1,2,4.5f,3,0.4f); for (int i = 0; i < 20; i++) { float theta = i*GrlConstants.F_PI*2.0f/20.0f; checkTangent(theta,rotated,delta); } } private void checkTangent( float t , EllipseRotated_F32 ellipse , float delta ) { Vector2D_F32 found = UtilEllipse_F32.computeTangent(t,ellipse,null); Vector2D_F32 expected = numericalTangent(t,ellipse,delta); float error0 = found.distance(expected); expected.x *= -1; expected.y *= -1; float error1 = found.distance(expected); float error = (float)Math.min(error0,error1); assertEquals(0,error,Math.sqrt(delta)); } private Vector2D_F32 numericalTangent( float t , EllipseRotated_F32 ellipse , float delta ) { Point2D_F32 a = UtilEllipse_F32.computePoint(t-delta,ellipse,null); Point2D_F32 b = UtilEllipse_F32.computePoint(t+delta,ellipse,null); Vector2D_F32 output = new Vector2D_F32(); output.x = (b.x-a.x)/(2.0f*delta); output.y = (b.y-a.y)/(2.0f*delta); output.normalize(); return output; } @Test public void tangentLines_point_ellipse() { // simple case with a circle at the origin checkTangentLines( -2,2, new EllipseRotated_F32(0,0,2,2,0)); checkTangentLines( 0,-10, new EllipseRotated_F32(0,0,2,2,0)); checkTangentLines( -10,0, new EllipseRotated_F32(0,0,2,2,0)); checkTangentLines( -10,1, new EllipseRotated_F32(0,0,2,2,0)); checkTangentLines( 1,-10, new EllipseRotated_F32(0,0,2,2,0)); // test failure case. Inside checkTangentLinesFail( 0,0, new EllipseRotated_F32(0,0,2,2,0)); checkTangentLinesFail( 0.05f,0, new EllipseRotated_F32(0,0,2,2,0)); checkTangentLinesFail( 0,0.1f, new EllipseRotated_F32(0,0,2,2,0)); checkTangentLinesFail( 0.05f,0.1f, new EllipseRotated_F32(0,0,2,2,0)); // same, but not circular checkTangentLines( -2,2, new EllipseRotated_F32(0,0,2,3,0)); checkTangentLines( 0,-10, new EllipseRotated_F32(0,0,2,3,0)); checkTangentLines( -10,0, new EllipseRotated_F32(0,0,2,3,0)); checkTangentLines( -10,1, new EllipseRotated_F32(0,0,2,3,0)); checkTangentLines( 1,-10, new EllipseRotated_F32(0,0,2,3,0)); // same, but translated float x = 1.2f, y = -0.5f; checkTangentLines( -2+x , 2 +y, new EllipseRotated_F32(x,y,2,3,0)); checkTangentLines( 0+x , -10+y, new EllipseRotated_F32(x,y,2,3,0)); checkTangentLines( -10+x, 0 +y, new EllipseRotated_F32(x,y,2,3,0)); checkTangentLines( -10+x, 1 +y, new EllipseRotated_F32(x,y,2,3,0)); checkTangentLines( 1+x , -10+y, new EllipseRotated_F32(x,y,2,3,0)); // same, but translated and rotated checkTangentLines( -3+x , 3 +y, new EllipseRotated_F32(x,y,2,3,0.1f)); checkTangentLines( 0+x , -10+y, new EllipseRotated_F32(x,y,2,3,0.1f)); checkTangentLines( -10+x, 0 +y, new EllipseRotated_F32(x,y,2,3,0.1f)); checkTangentLines( -10+x, 1 +y, new EllipseRotated_F32(x,y,2,3,0.1f)); checkTangentLines( 1+x , -10+y, new EllipseRotated_F32(x,y,2,3,0.1f)); } public void checkTangentLinesFail( float x, float y , EllipseRotated_F32 ellipse ) { Point2D_F32 pt = new Point2D_F32(x,y); Point2D_F32 pointA = new Point2D_F32(); Point2D_F32 pointB = new Point2D_F32(); assertFalse(UtilEllipse_F32.tangentLines(pt,ellipse,pointA,pointB)); } public void checkTangentLines( float x, float y , EllipseRotated_F32 ellipse ) { Point2D_F32 pt = new Point2D_F32(x,y); Point2D_F32 pointA = new Point2D_F32(); Point2D_F32 pointB = new Point2D_F32(); assertTrue(UtilEllipse_F32.tangentLines(pt,ellipse,pointA,pointB)); LineGeneral2D_F32 lineA = UtilLine2D_F32.convert(pt,pointA,(LineGeneral2D_F32)null); LineGeneral2D_F32 lineB = UtilLine2D_F32.convert(pt,pointB,(LineGeneral2D_F32)null); // the point should pass through both lines assertEquals(0, lineA.evaluate(pt.x,pt.y), GrlConstants.FLOAT_TEST_TOL); assertEquals(0, lineB.evaluate(pt.x,pt.y), GrlConstants.FLOAT_TEST_TOL); // if it's tangent there should only be one point of intersection Point2D_F32 pA = new Point2D_F32(); Point2D_F32 pB = new Point2D_F32(); assertTrue( 0 < Intersection2D_F32.intersection(lineA,ellipse,pA,pB, GrlConstants.FLOAT_TEST_TOL)); assertEquals(0,pA.distance(pB) , (float)Math.sqrt(GrlConstants.FLOAT_TEST_TOL) ); assertTrue( 0 < Intersection2D_F32.intersection(lineB,ellipse,pA,pB, GrlConstants.FLOAT_TEST_TOL)); assertEquals(0,pA.distance(pB) , (float)Math.sqrt(GrlConstants.FLOAT_TEST_TOL*20.0f) ); // Make sure the lines are not identical boolean idential = true; idential &= (float)Math.abs( lineA.A - lineB.A ) <= GrlConstants.FLOAT_TEST_TOL; idential &= (float)Math.abs( lineA.B - lineB.B ) <= GrlConstants.FLOAT_TEST_TOL; idential &= (float)Math.abs( lineA.C - lineB.C ) <= GrlConstants.FLOAT_TEST_TOL; assertFalse( idential ); } /** * Very basic unit test. The more rigerous one is in * {@link georegression.geometry.algs.TestTangentLinesTwoEllipses_F32} */ @Test public void tangentLines_ellipse_ellipse() { EllipseRotated_F32 ellipseA = new EllipseRotated_F32(0,1,4,2,0.1f); EllipseRotated_F32 ellipseB = new EllipseRotated_F32(-6,1.2f,1.5f,0.8f,-0.6f); Point2D_F32 tangentA0 = new Point2D_F32(); Point2D_F32 tangentA1 = new Point2D_F32(); Point2D_F32 tangentA2 = new Point2D_F32(); Point2D_F32 tangentA3 = new Point2D_F32(); Point2D_F32 tangentB0 = new Point2D_F32(); Point2D_F32 tangentB1 = new Point2D_F32(); Point2D_F32 tangentB2 = new Point2D_F32(); Point2D_F32 tangentB3 = new Point2D_F32(); UtilEllipse_F32.tangentLines(ellipseA,ellipseB, tangentA0,tangentA1,tangentA2,tangentA3, tangentB0,tangentB1,tangentB2,tangentB3); Point2D_F32 fooA0 = new Point2D_F32(); Point2D_F32 fooA1 = new Point2D_F32(); Point2D_F32 fooA2 = new Point2D_F32(); Point2D_F32 fooA3 = new Point2D_F32(); Point2D_F32 fooB0 = new Point2D_F32(); Point2D_F32 fooB1 = new Point2D_F32(); Point2D_F32 fooB2 = new Point2D_F32(); Point2D_F32 fooB3 = new Point2D_F32(); // see if it produces the same results as invoking the algorithm directly TangentLinesTwoEllipses_F32 alg = new TangentLinesTwoEllipses_F32(GrlConstants.FLOAT_TEST_TOL,10); alg.process(ellipseA,ellipseB, fooA0,fooA1,fooA2,fooA3, fooB0,fooB1,fooB2,fooB3); assertEquals( 0, fooA0.distance(tangentA0), GrlConstants.FLOAT_TEST_TOL); assertEquals( 0, fooA1.distance(tangentA1), GrlConstants.FLOAT_TEST_TOL); assertEquals( 0, fooA2.distance(tangentA2), GrlConstants.FLOAT_TEST_TOL); assertEquals( 0, fooA3.distance(tangentA3), GrlConstants.FLOAT_TEST_TOL); assertEquals( 0, fooB0.distance(tangentB0), GrlConstants.FLOAT_TEST_TOL); assertEquals( 0, fooB1.distance(tangentB1), GrlConstants.FLOAT_TEST_TOL); assertEquals( 0, fooB2.distance(tangentB2), GrlConstants.FLOAT_TEST_TOL); assertEquals( 0, fooB3.distance(tangentB3), GrlConstants.FLOAT_TEST_TOL); } }