/* * Copyright (C) 2011-2015, 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.metric.UtilAngle; import georegression.misc.GrlConstants; import georegression.misc.test.GeometryUnitTest; import georegression.struct.EulerType; import georegression.struct.point.Point3D_F32; import georegression.struct.so.Quaternion_F32; import georegression.struct.so.Rodrigues_F32; import org.ejml.UtilEjml; import org.ejml.data.DenseMatrix64F; import org.ejml.ops.CommonOps; import org.ejml.ops.MatrixFeatures; import org.ejml.ops.RandomMatrices; import org.junit.Test; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Random; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; /** * @author Peter Abeles */ public class TestConvertRotation3D_F32 { Random rand = new Random( 234234 ); @Test public void rodriguesToMatrix() { DenseMatrix64F rotZ = ConvertRotation3D_F32.rotZ( 0.5f, null ); Rodrigues_F32 r = new Rodrigues_F32( 0.5f, 0, 0, 1 ); DenseMatrix64F rod = ConvertRotation3D_F32.rodriguesToMatrix( r, null ); assertTrue( MatrixFeatures.isIdentical( rotZ, rod, GrlConstants.FLOAT_TEST_TOL ) ); } @Test public void rodriguesToEuler() throws InvocationTargetException, IllegalAccessException { somethingToEulerTest("rodriguesToEuler",rand); } public static void rodriguesToEuler(EulerType type , float rotA , float rotB , float rotC ) { DenseMatrix64F expected = ConvertRotation3D_F32.eulerToMatrix(type,rotA,rotB,rotC,null); Rodrigues_F32 rod = ConvertRotation3D_F32.matrixToRodrigues(expected,(Rodrigues_F32)null); float[] euler = ConvertRotation3D_F32.rodriguesToEuler(rod,type,null); DenseMatrix64F found = ConvertRotation3D_F32.eulerToMatrix(type,euler[0],euler[1],euler[2],null); DenseMatrix64F difference = new DenseMatrix64F(3,3); CommonOps.multTransB(expected,found,difference); assertTrue(MatrixFeatures.isIdentity(difference,Math.sqrt(GrlConstants.FLOAT_TEST_TOL))); } @Test public void rodriguesToQuaternion() { Rodrigues_F32 rod = new Rodrigues_F32(-1.5f,1,3,-4); Quaternion_F32 quat = ConvertRotation3D_F32.rodriguesToQuaternion(rod, null); DenseMatrix64F A = ConvertRotation3D_F32.quaternionToMatrix(quat, null); DenseMatrix64F B = ConvertRotation3D_F32.rodriguesToMatrix(rod, null); DenseMatrix64F C = new DenseMatrix64F(3,3); CommonOps.multTransA(A,B,C); assertTrue(MatrixFeatures.isIdentity(C, GrlConstants.FLOAT_TEST_TOL)); } @Test public void quaternionToRodrigues() { Quaternion_F32 quat = new Quaternion_F32(0.6f,2,3,-1); quat.normalize(); Rodrigues_F32 rod = ConvertRotation3D_F32.quaternionToRodrigues(quat,null); quat.normalize(); DenseMatrix64F A = ConvertRotation3D_F32.quaternionToMatrix(quat, null); DenseMatrix64F B = ConvertRotation3D_F32.rodriguesToMatrix(rod, null); DenseMatrix64F C = new DenseMatrix64F(3,3); CommonOps.multTransA(A,B,C); assertTrue(MatrixFeatures.isIdentity(C, GrlConstants.FLOAT_TEST_TOL)); } @Test public void quaternionToEuler() throws InvocationTargetException, IllegalAccessException { somethingToEulerTest("quaternionToEuler",rand); } public static void quaternionToEuler(EulerType type , float rotA , float rotB , float rotC ) { DenseMatrix64F expected = ConvertRotation3D_F32.eulerToMatrix(type,rotA,rotB,rotC,null); Quaternion_F32 q = ConvertRotation3D_F32.matrixToQuaternion(expected,null); float euler[] = ConvertRotation3D_F32.quaternionToEuler(q,type,null); DenseMatrix64F found = ConvertRotation3D_F32.eulerToMatrix(type,euler[0],euler[1],euler[2],null); DenseMatrix64F difference = new DenseMatrix64F(3,3); CommonOps.multTransB(expected,found,difference); assertTrue(MatrixFeatures.isIdentity(difference,Math.sqrt(GrlConstants.FLOAT_TEST_TOL))); } @Test public void matrixToQuaternion() { float pid2 = (float)Math.PI/2.0f; matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,0,0,0,null)); // single axis rotations matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,pid2,0,0,null)); matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,0,pid2,0,null)); matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,0,0,pid2,null)); matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,-pid2,0,0,null)); matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,0,-pid2,0,null)); matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,0,0,-pid2,null)); // two axis rotations which could be pathological matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,pid2,pid2,0,null)); matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,pid2,0,pid2,null)); matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,pid2,pid2,0,null)); matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,0,pid2,pid2,null)); matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,pid2,0,pid2,null)); matrixToQuaternion(ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,0,pid2,pid2,null)); for (int i = 0; i < 100; i++) { float rotX = 2.0f*rand.nextFloat() * (float)Math.PI - (float)Math.PI; float rotY = 2.0f*rand.nextFloat() * (float)Math.PI - (float)Math.PI; float rotZ = 2.0f*rand.nextFloat() * (float)Math.PI - (float)Math.PI; DenseMatrix64F R = ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,rotX,rotY,rotZ,null); matrixToQuaternion(R); } } public void matrixToQuaternion( DenseMatrix64F R ) { Quaternion_F32 q = ConvertRotation3D_F32.matrixToQuaternion(R,null); q.normalize(); DenseMatrix64F found = ConvertRotation3D_F32.quaternionToMatrix(q,null); DenseMatrix64F result = new DenseMatrix64F(3,3); CommonOps.multTransB(R,found,result); assertTrue(MatrixFeatures.isIdentity(result,Math.sqrt(GrlConstants.FLOAT_TEST_TOL))); } @Test public void matrixToRodrigues() { // create the rotation axis for( int i = 1; i < 20; i++ ) { float angle = i * (float)Math.PI / 20; checkMatrixToRodrigues( new Rodrigues_F32( angle, 0.1f, 2, 6 ) ); checkMatrixToRodrigues( new Rodrigues_F32( angle, 1, 0, 0 ) ); checkMatrixToRodrigues( new Rodrigues_F32( angle, 1, 1, 1 ) ); checkMatrixToRodrigues( new Rodrigues_F32( angle, -1, -1, -1 ) ); } // see how well it handles underflow checkMatrixToRodrigues( new Rodrigues_F32( 50*GrlConstants.FLOAT_TEST_TOL, -1, -1, -1 ) ); // test known pathological cases checkMatrixToRodrigues( new Rodrigues_F32( 0, 1, 1, 1 ), new Rodrigues_F32( 0, 1, 0, 0 ) ); checkMatrixToRodrigues( new Rodrigues_F32( (float)Math.PI / 2, 1, 1, 1 ), new Rodrigues_F32( (float)Math.PI/2, 1, 1, 1 ) ); checkMatrixToRodrigues( new Rodrigues_F32( (float)Math.PI, 1, 1, 1 ), new Rodrigues_F32( (float)Math.PI, 1, 1, 1 ) ); checkMatrixToRodrigues( new Rodrigues_F32( - (float)Math.PI, 1, 1, 1 ), new Rodrigues_F32( (float)Math.PI, 1, 1, 1 ) ); // edge case where diagonals sum up to 1 checkMatrixToRodrigues( new Rodrigues_F32( (float)Math.PI, 1, 0, 0 ) ); checkMatrixToRodrigues( new Rodrigues_F32( (float)Math.PI, 0, 1, 0 ) ); checkMatrixToRodrigues( new Rodrigues_F32( (float)Math.PI, 0, 0, 1 )); checkMatrixToRodrigues( (float)Math.PI, (float)Math.PI/2,0); checkMatrixToRodrigues( (float)Math.PI, (float)-Math.PI/2,0); checkMatrixToRodrigues( (float)Math.PI,0, (float)Math.PI/2); checkMatrixToRodrigues( (float)Math.PI,0, (float)-Math.PI/2); checkMatrixToRodrigues(0, (float)Math.PI, (float)Math.PI/2); checkMatrixToRodrigues(0, (float)Math.PI, (float)-Math.PI/2);; } private void checkMatrixToRodrigues( Rodrigues_F32 rodInput ) { // create the matrix using rodrigues DenseMatrix64F rod = ConvertRotation3D_F32.rodriguesToMatrix( rodInput, null ); // see if the vectors are the same Rodrigues_F32 found = ConvertRotation3D_F32.matrixToRodrigues( rod, (Rodrigues_F32)null ); // if the lines are parallel the dot product will be 1 or -1 float dot = found.unitAxisRotation.dot( rodInput.unitAxisRotation); assertEquals( 1, (float)Math.abs( dot ), GrlConstants.FLOAT_TEST_TOL ); // if the rotation vector is in the opposite direction then the found angle will be // the negative of the input. both are equivalent assertTrue(UtilAngle.dist(rodInput.theta * dot, found.theta) <= GrlConstants.FLOAT_TEST_TOL); } private void checkMatrixToRodrigues( float eulerX , float eulerY , float eulerZ ) { DenseMatrix64F M = ConvertRotation3D_F32.eulerToMatrix(EulerType.XYZ,eulerX,eulerY,eulerZ,null); Rodrigues_F32 rod = ConvertRotation3D_F32.matrixToRodrigues(M, (Rodrigues_F32)null); DenseMatrix64F found = ConvertRotation3D_F32.rodriguesToMatrix(rod,null); assertTrue(MatrixFeatures.isIdentical(M,found,1e-6)); } private void checkMatrixToRodrigues( Rodrigues_F32 input, Rodrigues_F32 expected ) { // create the matrix using rodrigues DenseMatrix64F rod = ConvertRotation3D_F32.rodriguesToMatrix( input, null ); // see if the vectors are the same Rodrigues_F32 found = ConvertRotation3D_F32.matrixToRodrigues( rod, (Rodrigues_F32)null ); // if the lines are parallel the dot product will be 1 or -1 assertEquals( 1, (float)Math.abs( found.unitAxisRotation.dot( expected.unitAxisRotation) ), GrlConstants.FLOAT_TEST_TOL ); // if the rotation vector is in the opposite direction then the found angle will be // the negative of the input. both are equivalent assertEquals( expected.theta, found.theta, 10.0f*GrlConstants.FLOAT_TEST_TOL ); } /** * A found test case where it failed */ @Test public void matrixToRodrigues_case0() { DenseMatrix64F R = UtilEjml.parseMatrix( "1.00000000000000000000e+00f -5.42066399999221260000e-14f -3.16267800000013500000e-13f \n" + "5.42066400000000000000e-14f 1.00000000000000040000e+00f 2.46136444559397200000e-13f \n" + "3.16267800000000000000e-13f -2.46191955710628460000e-13f 1.00000000000000040000e+00f", 3); Rodrigues_F32 found = ConvertRotation3D_F32.matrixToRodrigues( R, (Rodrigues_F32)null ); assertEquals(0,found.getTheta(),GrlConstants.FLOAT_TEST_TOL); } @Test public void matrixToRodrigues_case1() { DenseMatrix64F R = UtilEjml.parseMatrix( "0.99999999999999000000e+00f -5.42066399999221260000e-14f -3.16267800000013500000e-13f \n" + "5.42066400000000000000e-14f 0.99999999999999000000e+00f 2.46136444559397200000e-13f \n" + "3.16267800000000000000e-13f -2.46191955710628460000e-13f 0.99999999999999000000e+00f", 3); Rodrigues_F32 found = ConvertRotation3D_F32.matrixToRodrigues( R, (Rodrigues_F32)null ); assertEquals(0,found.getTheta(),50*GrlConstants.FLOAT_TEST_TOL ); } @Test public void rotX() { Point3D_F32 pt_y = new Point3D_F32( 0, 1.5f, 0 ); Point3D_F32 pt_z = new Point3D_F32( 0, 0, 1.5f ); DenseMatrix64F R = ConvertRotation3D_F32.rotX( (float)Math.PI / 2.0f, null ); GeometryMath_F32.mult( R, pt_y, pt_y ); GeometryMath_F32.mult( R, pt_z, pt_z ); assertTrue( pt_y.isIdentical( 0, 0, 1.5f, GrlConstants.FLOAT_TEST_TOL ) ); assertTrue( pt_z.isIdentical( 0, -1.5f, 0, GrlConstants.FLOAT_TEST_TOL ) ); } @Test public void rotY() { Point3D_F32 pt_x = new Point3D_F32( 1.5f, 0, 0 ); Point3D_F32 pt_z = new Point3D_F32( 0, 0, 1.5f ); DenseMatrix64F R = ConvertRotation3D_F32.rotY( (float)Math.PI / 2.0f, null ); GeometryMath_F32.mult( R, pt_x, pt_x ); GeometryMath_F32.mult( R, pt_z, pt_z ); assertTrue( pt_x.isIdentical( 0, 0, -1.5f, GrlConstants.FLOAT_TEST_TOL ) ); assertTrue( pt_z.isIdentical( 1.5f, 0, 0, GrlConstants.FLOAT_TEST_TOL ) ); } @Test public void rotZ() { Point3D_F32 pt_x = new Point3D_F32( 1.5f, 0, 0 ); Point3D_F32 pt_y = new Point3D_F32( 0, 1.5f, 0 ); DenseMatrix64F R = ConvertRotation3D_F32.rotZ( (float)Math.PI / 2.0f, null ); GeometryMath_F32.mult( R, pt_x, pt_x ); GeometryMath_F32.mult( R, pt_y, pt_y ); assertTrue( pt_x.isIdentical( 0, 1.5f, 0, GrlConstants.FLOAT_TEST_TOL ) ); assertTrue( pt_y.isIdentical( -1.5f, 0, 0, GrlConstants.FLOAT_TEST_TOL ) ); } @Test public void matrixToEuler() throws InvocationTargetException, IllegalAccessException { somethingToEulerTest("matrixToEuler",rand); } public static void matrixToEuler(EulerType type , float rotA , float rotB , float rotC ) { DenseMatrix64F expected = ConvertRotation3D_F32.eulerToMatrix(type,rotA,rotB,rotC,null); float euler[] = ConvertRotation3D_F32.matrixToEuler(expected,type,(float[])null); DenseMatrix64F found = ConvertRotation3D_F32.eulerToMatrix(type,euler[0],euler[1],euler[2],null); DenseMatrix64F difference = new DenseMatrix64F(3,3); CommonOps.multTransB(expected,found,difference); assertTrue(MatrixFeatures.isIdentity(difference,Math.sqrt(GrlConstants.FLOAT_TEST_TOL))); } /** * Creates a random matrix and sees if the approximation is a valid rotation matrix */ @Test public void approximateRotationMatrix_random() { DenseMatrix64F Q = RandomMatrices.createRandom( 3, 3, rand ); DenseMatrix64F R = ConvertRotation3D_F32.approximateRotationMatrix( Q, null ); assertTrue( MatrixFeatures.isOrthogonal( R, GrlConstants.FLOAT_TEST_TOL ) ); } /** * Create a rotation matrix and see if the exact same matrix is returned */ @Test public void approximateRotationMatrix_nochange() { DenseMatrix64F Q = RandomMatrices.createOrthogonal( 3, 3, rand ); DenseMatrix64F R = ConvertRotation3D_F32.approximateRotationMatrix( Q, null ); assertTrue( MatrixFeatures.isIdentical( Q, R, GrlConstants.FLOAT_TEST_TOL ) ); } @Test public void eulerToMatrix() throws InvocationTargetException, IllegalAccessException { somethingToEulerTest("eulerToMatrix",rand); } public static void eulerToMatrix(EulerType type , float rotA , float rotB , float rotC ) { DenseMatrix64F matA = rotateAxis(type.getAxisA(),rotA); DenseMatrix64F matB = rotateAxis(type.getAxisB(),rotB); DenseMatrix64F matC = rotateAxis(type.getAxisC(),rotC); DenseMatrix64F matEuler = ConvertRotation3D_F32.eulerToMatrix(type,rotA,rotB,rotC,null); Point3D_F32 a = new Point3D_F32(1,2,3); Point3D_F32 tmp = new Point3D_F32(); Point3D_F32 expected = new Point3D_F32(); Point3D_F32 found = new Point3D_F32(); GeometryMath_F32.mult(matA,a,expected); GeometryMath_F32.mult(matB,expected,tmp); GeometryMath_F32.mult(matC,tmp,expected); GeometryMath_F32.mult(matEuler,a,found); assertTrue(expected.distance(found) < GrlConstants.FLOAT_TEST_TOL); } private static DenseMatrix64F rotateAxis( int which , float angle ) { if( which == 0 ) return ConvertRotation3D_F32.rotX(angle,null); else if( which == 1 ) return ConvertRotation3D_F32.rotY(angle,null); else return ConvertRotation3D_F32.rotZ(angle,null); } @Test public void eulerToQuaternion() throws InvocationTargetException, IllegalAccessException { somethingToEulerTest("eulerToQuaternion",rand); } public static void eulerToQuaternion(EulerType type , float rotA , float rotB , float rotC ) { DenseMatrix64F expected = ConvertRotation3D_F32.eulerToMatrix(type,rotA,rotB,rotC,null); Quaternion_F32 q = new Quaternion_F32(); ConvertRotation3D_F32.eulerToQuaternion(type,rotA,rotB,rotC,q); DenseMatrix64F found = ConvertRotation3D_F32.quaternionToMatrix(q,null); DenseMatrix64F result = new DenseMatrix64F(3,3); CommonOps.multTransB(expected,found,result); assertTrue(MatrixFeatures.isIdentity(result, (float)Math.sqrt(GrlConstants.FLOAT_TEST_TOL))); } /** * Tests quaternions using the following property: * <p/> * q = cos(a/2) + u*sin(a/2) * <p/> * where 'a' is the angle of rotation, u is the unit axis of rotation. */ @Test public void quaternionToMatrix() { // rotate around z-axis 90 degrees Quaternion_F32 q = ConvertRotation3D_F32.rodriguesToQuaternion( new Rodrigues_F32( (float)Math.PI / 2.0f, 0, 0, 1 ), null ); DenseMatrix64F R = ConvertRotation3D_F32.quaternionToMatrix( q, null ); Point3D_F32 p = new Point3D_F32( 1, 0, 0 ); GeometryMath_F32.mult( R, p, p ); GeometryUnitTest.assertEquals( p, 0, 1, 0, GrlConstants.FLOAT_TEST_TOL ); // rotate around y-axis 90 degrees q = ConvertRotation3D_F32.rodriguesToQuaternion( new Rodrigues_F32( (float)Math.PI / 2.0f, 0, 1, 0 ), null ); q.normalize(); R = ConvertRotation3D_F32.quaternionToMatrix( q, R ); p.set( 1, 0, 0 ); GeometryMath_F32.mult( R, p, p ); GeometryUnitTest.assertEquals( p, 0, 0, -1, GrlConstants.FLOAT_TEST_TOL ); for (int i = 0; i < 30; i++) { Rodrigues_F32 rod = new Rodrigues_F32(); rod.theta = (float)Math.PI*(2*rand.nextFloat()-1); rod.setParamVector(rand.nextFloat()-0.5f,rand.nextFloat()-0.5f,rand.nextFloat()-0.5f); rod.unitAxisRotation.normalize(); q = ConvertRotation3D_F32.rodriguesToQuaternion( rod, null ); q.normalize(); DenseMatrix64F expected = ConvertRotation3D_F32.rodriguesToMatrix( rod, null ); DenseMatrix64F found = ConvertRotation3D_F32.quaternionToMatrix( q, null ); DenseMatrix64F difference = new DenseMatrix64F(3,3); CommonOps.multTransB(expected,found,difference); assertTrue(MatrixFeatures.isIdentity(difference,GrlConstants.FLOAT_TEST_TOL)); } } /** * Standard checks for converting one parameterization into Euler. Checks special cases with rotations of 90 and * 180 degrees plus random ones */ public static void somethingToEulerTest(String functionName , Random rand ) throws InvocationTargetException, IllegalAccessException { Method m; try { m = TestConvertRotation3D_F32.class.getMethod(functionName,EulerType.class,float.class,float.class,float.class); } catch (NoSuchMethodException e) { throw new RuntimeException(e.getMessage()); } for(EulerType type : EulerType.values() ) { // System.out.println("type = "+type); float PId2 = (float)Math.PI/2.0f; float PI = (float)Math.PI; m.invoke(null,type,0,0,0); // single axis m.invoke(null,type,PId2,0,0); m.invoke(null,type,0,PId2,0); m.invoke(null,type,0,0,PId2); m.invoke(null,type,-PId2,0,0); m.invoke(null,type,0,-PId2,0); m.invoke(null,type,0,0,-PId2); m.invoke(null,type,PI,0,0); m.invoke(null,type,0,0,PI); m.invoke(null,type,-PI,0,0); m.invoke(null,type,0,0,-PI); // two axis maybe pathological for (int i = 0; i < 4; i++) { float sgn0 = i%2==0?1:-1; float sgn1 = i/2==0?1:-1; // System.out.println(sgn0+" "+sgn1); m.invoke(null,type,sgn0*PId2,sgn1*PId2,0); m.invoke(null,type,sgn0*PId2,0,sgn1*PId2); m.invoke(null,type,sgn0*PId2,sgn1*PId2,0); m.invoke(null,type,0,sgn0*PId2,sgn1*PId2); m.invoke(null,type,sgn0*PId2,0,sgn1*PId2); m.invoke(null,type,0,sgn0*PId2,sgn1*PId2); m.invoke(null,type,sgn0*PI,sgn1*PId2,0); m.invoke(null,type,sgn0*PI,0,sgn1*PI); m.invoke(null,type,sgn0*PI,sgn1*PId2,0); m.invoke(null,type,0,sgn0*PId2,sgn1*PI); m.invoke(null,type,sgn0*PI,0,sgn1*PI); m.invoke(null,type,0,sgn0*PId2,sgn1*PI); } for (int i = 0; i < 30; i++) { float rotA = 2.0f*rand.nextFloat() * (float)Math.PI - (float)Math.PI; float rotB = rand.nextFloat() * (float)Math.PI - (float)Math.PI/2.0f; float rotC = 2.0f*rand.nextFloat() * (float)Math.PI - (float)Math.PI; m.invoke(null,type,rotA,rotB,rotC); } } } }