| // Ceres Solver - A fast non-linear least squares minimizer |
| // Copyright 2023 Google Inc. All rights reserved. |
| // http://ceres-solver.org/ |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // * Redistributions of source code must retain the above copyright notice, |
| // this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // * Neither the name of Google Inc. nor the names of its contributors may be |
| // used to endorse or promote products derived from this software without |
| // specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE |
| // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| // POSSIBILITY OF SUCH DAMAGE. |
| // |
| // Author: mierle@gmail.com (Keir Mierle) |
| // |
| // WARNING WARNING WARNING |
| // WARNING WARNING WARNING Tiny solver is experimental and will change. |
| // WARNING WARNING WARNING |
| // |
| // A tiny least squares solver using Levenberg-Marquardt, intended for solving |
| // small dense problems with low latency and low overhead. The implementation |
| // takes care to do all allocation up front, so that no memory is allocated |
| // during solving. This is especially useful when solving many similar problems; |
| // for example, inverse pixel distortion for every pixel on a grid. |
| // |
| // Note: This code has no dependencies beyond Eigen, including on other parts of |
| // Ceres, so it is possible to take this file alone and put it in another |
| // project without the rest of Ceres. |
| // |
| // Algorithm based off of: |
| // |
| // [1] K. Madsen, H. Nielsen, O. Tingleoff. |
| // Methods for Non-linear Least Squares Problems. |
| // http://www2.imm.dtu.dk/pubdb/views/edoc_download.php/3215/pdf/imm3215.pdf |
| |
| #ifndef CERES_PUBLIC_TINY_SOLVER_H_ |
| #define CERES_PUBLIC_TINY_SOLVER_H_ |
| |
| #include <cassert> |
| #include <cmath> |
| |
| #include "Eigen/Dense" |
| |
| namespace ceres { |
| |
| // To use tiny solver, create a class or struct that allows computing the cost |
| // function (described below). This is similar to a ceres::CostFunction, but is |
| // different to enable statically allocating all memory for the solver |
| // (specifically, enum sizes). Key parts are the Scalar typedef, the enums to |
| // describe problem sizes (needed to remove all heap allocations), and the |
| // operator() overload to evaluate the cost and (optionally) jacobians. |
| // |
| // struct TinySolverCostFunctionTraits { |
| // typedef double Scalar; |
| // enum { |
| // NUM_RESIDUALS = <int> OR Eigen::Dynamic, |
| // NUM_PARAMETERS = <int> OR Eigen::Dynamic, |
| // }; |
| // bool operator()(const double* parameters, |
| // double* residuals, |
| // double* jacobian) const; |
| // |
| // int NumResiduals() const; -- Needed if NUM_RESIDUALS == Eigen::Dynamic. |
| // int NumParameters() const; -- Needed if NUM_PARAMETERS == Eigen::Dynamic. |
| // }; |
| // |
| // For operator(), the size of the objects is: |
| // |
| // double* parameters -- NUM_PARAMETERS or NumParameters() |
| // double* residuals -- NUM_RESIDUALS or NumResiduals() |
| // double* jacobian -- NUM_RESIDUALS * NUM_PARAMETERS in column-major format |
| // (Eigen's default); or nullptr if no jacobian |
| // requested. |
| // |
| // An example (fully statically sized): |
| // |
| // struct MyCostFunctionExample { |
| // typedef double Scalar; |
| // enum { |
| // NUM_RESIDUALS = 2, |
| // NUM_PARAMETERS = 3, |
| // }; |
| // bool operator()(const double* parameters, |
| // double* residuals, |
| // double* jacobian) const { |
| // residuals[0] = x + 2*y + 4*z; |
| // residuals[1] = y * z; |
| // if (jacobian) { |
| // jacobian[0 * 2 + 0] = 1; // First column (x). |
| // jacobian[0 * 2 + 1] = 0; |
| // |
| // jacobian[1 * 2 + 0] = 2; // Second column (y). |
| // jacobian[1 * 2 + 1] = z; |
| // |
| // jacobian[2 * 2 + 0] = 4; // Third column (z). |
| // jacobian[2 * 2 + 1] = y; |
| // } |
| // return true; |
| // } |
| // }; |
| // |
| // The solver supports either statically or dynamically sized cost |
| // functions. If the number of residuals is dynamic then the Function |
| // must define: |
| // |
| // int NumResiduals() const; |
| // |
| // If the number of parameters is dynamic then the Function must |
| // define: |
| // |
| // int NumParameters() const; |
| // |
| template <typename Function, |
| typename LinearSolver = |
| Eigen::LDLT<Eigen::Matrix<typename Function::Scalar, // |
| Function::NUM_PARAMETERS, // |
| Function::NUM_PARAMETERS>>> |
| class TinySolver { |
| public: |
| // This class needs to have an Eigen aligned operator new as it contains |
| // fixed-size Eigen types. |
| EIGEN_MAKE_ALIGNED_OPERATOR_NEW |
| |
| enum { |
| NUM_RESIDUALS = Function::NUM_RESIDUALS, |
| NUM_PARAMETERS = Function::NUM_PARAMETERS |
| }; |
| using Scalar = typename Function::Scalar; |
| using Parameters = typename Eigen::Matrix<Scalar, NUM_PARAMETERS, 1>; |
| |
| enum Status { |
| // max_norm |J'(x) * f(x)| < gradient_tolerance |
| GRADIENT_TOO_SMALL, |
| // ||dx|| <= parameter_tolerance * (||x|| + parameter_tolerance) |
| RELATIVE_STEP_SIZE_TOO_SMALL, |
| // cost_threshold > ||f(x)||^2 / 2 |
| COST_TOO_SMALL, |
| // num_iterations >= max_num_iterations |
| HIT_MAX_ITERATIONS, |
| // (new_cost - old_cost) < function_tolerance * old_cost |
| COST_CHANGE_TOO_SMALL, |
| |
| // TODO(sameeragarwal): Deal with numerical failures. |
| }; |
| |
| struct Options { |
| int max_num_iterations = 50; |
| |
| // max_norm |J'(x) * f(x)| < gradient_tolerance |
| Scalar gradient_tolerance = 1e-10; |
| |
| // ||dx|| <= parameter_tolerance * (||x|| + parameter_tolerance) |
| Scalar parameter_tolerance = 1e-8; |
| |
| // (new_cost - old_cost) < function_tolerance * old_cost |
| Scalar function_tolerance = 1e-6; |
| |
| // cost_threshold > ||f(x)||^2 / 2 |
| Scalar cost_threshold = std::numeric_limits<Scalar>::epsilon(); |
| |
| Scalar initial_trust_region_radius = 1e4; |
| }; |
| |
| struct Summary { |
| // 1/2 ||f(x_0)||^2 |
| Scalar initial_cost = -1; |
| // 1/2 ||f(x)||^2 |
| Scalar final_cost = -1; |
| // max_norm(J'f(x)) |
| Scalar gradient_max_norm = -1; |
| int iterations = -1; |
| Status status = HIT_MAX_ITERATIONS; |
| }; |
| |
| bool Update(const Function& function, const Parameters& x) { |
| if (!function(x.data(), residuals_.data(), jacobian_.data())) { |
| return false; |
| } |
| |
| residuals_ = -residuals_; |
| |
| // On the first iteration, compute a diagonal (Jacobi) scaling |
| // matrix, which we store as a vector. |
| if (summary.iterations == 0) { |
| // jacobi_scaling = 1 / (1 + diagonal(J'J)) |
| // |
| // 1 is added to the denominator to regularize small diagonal |
| // entries. |
| jacobi_scaling_ = 1.0 / (1.0 + jacobian_.colwise().norm().array()); |
| } |
| |
| // This explicitly computes the normal equations, which is numerically |
| // unstable. Nevertheless, it is often good enough and is fast. |
| // |
| // TODO(sameeragarwal): Refactor this to allow for DenseQR |
| // factorization. |
| jacobian_ = jacobian_ * jacobi_scaling_.asDiagonal(); |
| jtj_ = jacobian_.transpose() * jacobian_; |
| g_ = jacobian_.transpose() * residuals_; |
| summary.gradient_max_norm = g_.array().abs().maxCoeff(); |
| cost_ = residuals_.squaredNorm() / 2; |
| return true; |
| } |
| |
| const Summary& Solve(const Function& function, Parameters* x_and_min) { |
| Initialize<NUM_RESIDUALS, NUM_PARAMETERS>(function); |
| assert(x_and_min); |
| Parameters& x = *x_and_min; |
| summary = Summary(); |
| summary.iterations = 0; |
| |
| // TODO(sameeragarwal): Deal with failure here. |
| Update(function, x); |
| summary.initial_cost = cost_; |
| summary.final_cost = cost_; |
| |
| if (summary.gradient_max_norm < options.gradient_tolerance) { |
| summary.status = GRADIENT_TOO_SMALL; |
| return summary; |
| } |
| |
| if (cost_ < options.cost_threshold) { |
| summary.status = COST_TOO_SMALL; |
| return summary; |
| } |
| |
| Scalar u = 1.0 / options.initial_trust_region_radius; |
| Scalar v = 2; |
| |
| for (summary.iterations = 1; |
| summary.iterations < options.max_num_iterations; |
| summary.iterations++) { |
| jtj_regularized_ = jtj_; |
| const Scalar min_diagonal = 1e-6; |
| const Scalar max_diagonal = 1e32; |
| for (int i = 0; i < dx_.rows(); ++i) { |
| jtj_regularized_(i, i) += |
| u * (std::min)((std::max)(jtj_(i, i), min_diagonal), max_diagonal); |
| } |
| |
| // TODO(sameeragarwal): Check for failure and deal with it. |
| linear_solver_.compute(jtj_regularized_); |
| lm_step_ = linear_solver_.solve(g_); |
| dx_ = jacobi_scaling_.asDiagonal() * lm_step_; |
| |
| // Adding parameter_tolerance to x.norm() ensures that this |
| // works if x is near zero. |
| const Scalar parameter_tolerance = |
| options.parameter_tolerance * |
| (x.norm() + options.parameter_tolerance); |
| if (dx_.norm() < parameter_tolerance) { |
| summary.status = RELATIVE_STEP_SIZE_TOO_SMALL; |
| break; |
| } |
| x_new_ = x + dx_; |
| |
| // TODO(keir): Add proper handling of errors from user eval of cost |
| // functions. |
| function(&x_new_[0], &f_x_new_[0], nullptr); |
| |
| const Scalar cost_change = (2 * cost_ - f_x_new_.squaredNorm()); |
| // TODO(sameeragarwal): Better more numerically stable evaluation. |
| const Scalar model_cost_change = lm_step_.dot(2 * g_ - jtj_ * lm_step_); |
| |
| // rho is the ratio of the actual reduction in error to the reduction |
| // in error that would be obtained if the problem was linear. See [1] |
| // for details. |
| Scalar rho(cost_change / model_cost_change); |
| if (rho > 0) { |
| // Accept the Levenberg-Marquardt step because the linear |
| // model fits well. |
| x = x_new_; |
| |
| if (std::abs(cost_change) < options.function_tolerance) { |
| cost_ = f_x_new_.squaredNorm() / 2; |
| summary.status = COST_CHANGE_TOO_SMALL; |
| break; |
| } |
| |
| // TODO(sameeragarwal): Deal with failure. |
| Update(function, x); |
| if (summary.gradient_max_norm < options.gradient_tolerance) { |
| summary.status = GRADIENT_TOO_SMALL; |
| break; |
| } |
| |
| if (cost_ < options.cost_threshold) { |
| summary.status = COST_TOO_SMALL; |
| break; |
| } |
| |
| Scalar tmp = Scalar(2 * rho - 1); |
| u = u * (std::max)(Scalar(1 / 3.), Scalar(1) - tmp * tmp * tmp); |
| v = 2; |
| |
| } else { |
| // Reject the update because either the normal equations failed to solve |
| // or the local linear model was not good (rho < 0). |
| |
| // Additionally if the cost change is too small, then terminate. |
| if (std::abs(cost_change) < options.function_tolerance) { |
| // Terminate |
| summary.status = COST_CHANGE_TOO_SMALL; |
| break; |
| } |
| |
| // Reduce the size of the trust region. |
| u *= v; |
| v *= 2; |
| } |
| } |
| |
| summary.final_cost = cost_; |
| return summary; |
| } |
| |
| Options options; |
| Summary summary; |
| |
| private: |
| // Preallocate everything, including temporary storage needed for solving the |
| // linear system. This allows reusing the intermediate storage across solves. |
| LinearSolver linear_solver_; |
| Scalar cost_; |
| Parameters dx_, x_new_, g_, jacobi_scaling_, lm_step_; |
| Eigen::Matrix<Scalar, NUM_RESIDUALS, 1> residuals_, f_x_new_; |
| Eigen::Matrix<Scalar, NUM_RESIDUALS, NUM_PARAMETERS> jacobian_; |
| Eigen::Matrix<Scalar, NUM_PARAMETERS, NUM_PARAMETERS> jtj_, jtj_regularized_; |
| |
| // The following definitions are needed for template metaprogramming. |
| template <bool Condition, typename T> |
| struct enable_if; |
| |
| template <typename T> |
| struct enable_if<true, T> { |
| using type = T; |
| }; |
| |
| // The number of parameters and residuals are dynamically sized. |
| template <int R, int P> |
| typename enable_if<(R == Eigen::Dynamic && P == Eigen::Dynamic), void>::type |
| Initialize(const Function& function) { |
| Initialize(function.NumResiduals(), function.NumParameters()); |
| } |
| |
| // The number of parameters is dynamically sized and the number of |
| // residuals is statically sized. |
| template <int R, int P> |
| typename enable_if<(R == Eigen::Dynamic && P != Eigen::Dynamic), void>::type |
| Initialize(const Function& function) { |
| Initialize(function.NumResiduals(), P); |
| } |
| |
| // The number of parameters is statically sized and the number of |
| // residuals is dynamically sized. |
| template <int R, int P> |
| typename enable_if<(R != Eigen::Dynamic && P == Eigen::Dynamic), void>::type |
| Initialize(const Function& function) { |
| Initialize(R, function.NumParameters()); |
| } |
| |
| // The number of parameters and residuals are statically sized. |
| template <int R, int P> |
| typename enable_if<(R != Eigen::Dynamic && P != Eigen::Dynamic), void>::type |
| Initialize(const Function& /* function */) {} |
| |
| void Initialize(int num_residuals, int num_parameters) { |
| dx_.resize(num_parameters); |
| x_new_.resize(num_parameters); |
| g_.resize(num_parameters); |
| jacobi_scaling_.resize(num_parameters); |
| lm_step_.resize(num_parameters); |
| residuals_.resize(num_residuals); |
| f_x_new_.resize(num_residuals); |
| jacobian_.resize(num_residuals, num_parameters); |
| jtj_.resize(num_parameters, num_parameters); |
| jtj_regularized_.resize(num_parameters, num_parameters); |
| } |
| }; |
| |
| } // namespace ceres |
| |
| #endif // CERES_PUBLIC_TINY_SOLVER_H_ |