/* * 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.fitting.ellipse; import georegression.struct.point.Point2D_F64; import georegression.struct.shapes.EllipseQuadratic_F64; import org.ejml.data.DenseMatrix64F; import org.ejml.factory.DecompositionFactory; import org.ejml.factory.LinearSolverFactory; import org.ejml.interfaces.decomposition.EigenDecomposition; import org.ejml.interfaces.linsol.LinearSolver; import org.ejml.ops.CommonOps; import java.util.List; /** * <p> * Fits an ellipse to a set of points in "closed form" by minimizing algebraic least-squares error. The method used is * described in [1] and is a repartitioning of the solution describe in [2], with the aim of improving numerical * stability. The found ellipse is described using 6 coefficients, as is shown below. * {@code F(x,y) = a*x^2 + 2*b*x*y + c*y^2 + 2*d*x + 2*e*y + f = 0 and b^2 - 4*ac < 0} * <p> * * <p> * One peculiarity of this algorithm is that it's less stable when perfect data is provided. This instability became * evident when constructing unit tests and some of them failed. Tests on the original Matlab code also failed. * </p> * * <ul> * <li>[1] Radim Halir and Jan Flusser, "Numerically Stable Direct Least Squares Fitting Of Ellipses" 1998</li> * <li>[2] Fitzgibbon, A. W., Pilu, M and Fischer, R. B.: "Direct least squares fitting of ellipses" * Technical Report DAIRP-794, Department of Artificial Intelligence, The University of Edinburgh, January 1996</li> * </ul> * * @author Peter Abeles */ public class FitEllipseAlgebraic { // qudratic part of design matrix private DenseMatrix64F D1 = new DenseMatrix64F(3,1); // linear part of design matrix private DenseMatrix64F D2 = new DenseMatrix64F(3,1); // quadratic part of scatter matrix private DenseMatrix64F S1 = new DenseMatrix64F(3,3); // combined part of scatter matrix private DenseMatrix64F S2 = new DenseMatrix64F(3,3); //linear part of scatter matrix private DenseMatrix64F S3 = new DenseMatrix64F(3,3); // Reduced scatter matrix private DenseMatrix64F M = new DenseMatrix64F(3,3); // storage for intermediate steps private DenseMatrix64F T = new DenseMatrix64F(3,3); private DenseMatrix64F Ta1 = new DenseMatrix64F(3,1); private DenseMatrix64F S2_tran = new DenseMatrix64F(3,3); private LinearSolver<DenseMatrix64F> solver = LinearSolverFactory.linear(3); private EigenDecomposition<DenseMatrix64F> eigen = DecompositionFactory.eig(3,true,false); private EllipseQuadratic_F64 ellipse = new EllipseQuadratic_F64(); public boolean process( List<Point2D_F64> points ) { int N = points.size(); // Construct the design matrices. linear and quadratic D1.reshape(N,3); D2.reshape(N,3); int index = 0; for( int i = 0; i < N; i++ ) { Point2D_F64 p = points.get(i); // fill in each row one at a time D1.data[index] = p.x*p.x; D2.data[index++] = p.x; D1.data[index] = p.x*p.y; D2.data[index++] = p.y; D1.data[index] = p.y*p.y; D2.data[index++] = 1; } // Compute scatter matrix CommonOps.multTransA(D1, D1, S1); // S1 = D1'*D1 CommonOps.multTransA(D1, D2, S2); // S2 = D1'*D2 CommonOps.multTransA(D2, D2, S3); // S3 = D2'*D2 // for getting a2 from a1 // T = -inv(S3)*S2' if( !solver.setA(S3) ) return false; CommonOps.transpose(S2,S2_tran); CommonOps.changeSign(S2_tran); solver.solve(S2_tran, T); // Compute reduced scatter matrix // M = S1 + S2*T CommonOps.mult(S2, T, M); CommonOps.add(M,S1,M); // Premultiply by inv(C1). inverse of constraint matrix for( int col = 0; col < 3; col++ ) { double m0 = M.unsafe_get(0, col); double m1 = M.unsafe_get(1, col); double m2 = M.unsafe_get(2, col); M.unsafe_set(0,col, m2 / 2); M.unsafe_set(1,col, -m1); M.unsafe_set(2,col, m0 / 2); } if( !eigen.decompose(M) ) return false; DenseMatrix64F a1 = selectBestEigenVector(); if( a1 == null ) return false; // ellipse coefficients CommonOps.mult(T,a1,Ta1); ellipse.a = a1.data[0]; ellipse.b = a1.data[1]/2; ellipse.c = a1.data[2]; ellipse.d = Ta1.data[0]/2; ellipse.e = Ta1.data[1]/2; ellipse.f = Ta1.data[2]; return true; } private DenseMatrix64F selectBestEigenVector() { int bestIndex = -1; double bestCond = Double.MAX_VALUE; for( int i = 0; i < eigen.getNumberOfEigenvalues(); i++ ) { DenseMatrix64F v = eigen.getEigenVector(i); if( v == null ) // TODO WTF?!?! continue; // evaluate a'*C*a = 1 double cond = 4*v.get(0)*v.get(2) - v.get(1)*v.get(1); double condError = (cond - 1)*(cond - 1); if( cond > 0 && condError < bestCond ) { bestCond = condError; bestIndex = i; } } if( bestIndex == -1 ) return null; return eigen.getEigenVector(bestIndex); } public EllipseQuadratic_F64 getEllipse() { return ellipse; } }