Closed svwilliams closed 7 months ago
Let's consider some use case:
Let's consider some use case:
fuse
from source code. I can't think of a way around this.fuse
interface remains unchanged. If the user code does include a custom variable, then I would recommend implementing the monifold()
interface instead of the localParameterization()
. However, this is not required. Newly created variables with localParameterization()
functions will continue to operate correctly and automatically through the ManifoldAdapter class.
Additional fixes and cleanup for #341.
This PR has a slightly strange history. It was started as a pull request from the community. Ultimately I needed to do some additional refactoring before I was happy with its state. But I cannot push changes to a community member's fork. So this PR includes all of the commits from #341 and adds an additional dozen or so commits from me at the end. I was pondering having separate reviews for the two pieces, but ultimately it is probably easier to understand as a single change set. When it comes time to merge it, I may merge #341 and then rebase this branch on top. Mostly to maintain proper attribution.
Anyway, what is this PR for?
The
fuse
library is built on top of a nonlinear least-squares solver, Google Ceres Solver. One of the concepts in Ceres Solver is a "local parameterization". This models the concept of variable updates that are nonlinear. The classic example is SO3, where a 3D rotation is represented by a 4-vector quaternion even though there are only 3 degrees of freedom. Further, all rotations must lie on the unit sphere in the 4D space. When the optimizer computes an update to a 3D rotation variable, it is basically computing a direction and step size to apply. This update can be thought of as living on a plane tangent to the 4D sphere at the starting rotation. And in this case, that tangent plane is only 3 dimensions since there are only 3 degrees of freedom. The LocalParameterization class is responsible for projecting the 3D linear update back onto the 4D nonlinear surface/manifold. Here's a more formal description if you don't like my incoherent rambling: http://ceres-solver.org/nnls_modeling.html#manifoldIn Ceres Solver version 2.0 and before, the LocalParameterization class only handled projecting the linear delta from the tangent plane back onto the nonlinear manifold. It provided a method:
Plus(x, delta) -> x_plus_delta
, wherex
is the starting point/linearization point in the 4D quaternion space,delta
is a displacement in the 3D tangent space, and the output is new rotation in 4D quaternion space. Ceres Solver did not provide a method to project a difference onto the tangent plane; basically the inverse operation:Minus(x, x_plus_delta) -> delta
. However, thefuse
project needed the inverse operation, so I added methods for that infuse_core::LocalParameterization
that was derived from the Ceres Solver class of the same name.In Release 2.1, Ceres Solver decided it needed the inverse operation as well. Ceres added a new class,
ceres::Manifold
that implements both aPlus()
andMinus()
method, and changes a few function names as well.In Release 2.2, Ceres Solver removed the LocalParameterization class entirely. This means that
fuse
needs to be updated to use theManifold
base class instead of theLocalParameterization
base class. Merely updating the code is actually trivial. The problems are:fuse
code needs to support multiple versions of Ceres Solver.And how does the PR work?
My first instinct was to change the
fuse::LocalParameterization
class to derive fromceres::Manifold
instead ofceres::LocalParameterization
. The fuse base class could implement the translation logic required and derived classes would not need to be updated. However, there is a technical issue:When I added the
Minus()
functionality tofuse::LocalParameterization
, I selected the exact same name as theceres::Manifold
class. Further, the function signatures are identical:Minus(const double*, const double*, double*)
. But the parameter orders are different:Minus(x, y) -> y_minus_x
Minus(y, x) -> y_minus_x
Because the function signatures are the same but the parameter meanings are different, there is no way to implement the translation logic. The derived classMinus()
implementation using the Fuse parameter order will be the virtual function override of the CeresMinus()
function.Instead, this PR:
fuse::Manifold
class the derives from theceres::Manifold
classManifoldAdapter
that accepts afuse::LocalParameterization
object and implements thefuse::Manifold
interface using that stored object.manifold()
function to the Variable class in addition to thelocalParameterization()
method. The default implementation of that method calls thelocalParameterization()
function and wraps the returned object in aManifoldAdapter
. Thus, any legacy code that does not implement themanifold()
function directly will continue to run correctly using the adapter.fuse_variables
repo to implement the required Manifold classes so that the main fuse code does use the ManifoldAdapter.#if CERES_SUPPORTS_MANIFOLDS
clauses everywhere to switch functionality based on the version of CeresSolver in use.