Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

New LevMarq implementation#21018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
alalek merged 66 commits intoopencv:5.xfromsavuor:levmarqfromscratch
Dec 27, 2021
Merged

Conversation

@savuor
Copy link
Contributor

@savuorsavuor commentedNov 7, 2021
edited by alalek
Loading

Merge with extra:opencv/opencv_extra#942
Merge with contrib:opencv/opencv_contrib#3127

TODOs:

  • new interface: constructors,run(params) for linear, various factories, return report after optimization
  • (optional) implement geodesic acceleration
  • play with the params on existing code to find better combinations
  • add perf reports
  • remove old impl
  • remove allDEBUG code and fulfill allTODO statements

What?

NewLevenberg-Marquadt algorithm implementation replaces the old one.

Why?

Because the old impl isn't as good as we want:

  • No sparse matrix support
  • No SO(3), SE(3) or any other manifold type supported for optimized params
  • No flexibility: fixed up/down coefficients and some deltas, a few options for a termination criteria

This stops it from using in some complex 3d-related problems such as pose graph optimization.

An algorithm termination deserves its own list offeatures:

  • Iteration calculation isn't fair: only successful ones are counted, a real amount of iterations elapsed can be several times bigger than reported
  • The optimization can stop and be considered successful even regardless of successful iterations elapsed count, cost function value drop or NaNs in a gradient
  • Step norm threshold is tied up to energy threshold

This is not OK even for a basic LevMarq solver.

How?

A new code fixes everything:

  • A base class does not depend on a type, layout or a group structure of a param vector or an objective function jacobian. A child class should provide a storage for that data and implement all virtual member functions that process it. This lets a user to use sparse matrices, exponential increments or fixed variables.
  • The algorithm is highly tunable:
    • Lambda initial/up/down values can be changed by a user, diagonal clamping or upFactor doubling can be turned on/off
    • A termination criteria can be composed of the following ones, each threshold is independent and tunable:
      • relative energy delta < threshold
      • gradient max value < threshold
      • step norm (L2 or Inf) < threshold
      • cost function < threshold
      • iterations count < maxIterations
  • Jacobian scaling, step quality metric and geodesic acceleration supported, they can improve the algorithm's speed/stability sometimes
  • A dense linear implementation converges at least as good as the old code does

This code is based on a current pose graph optimization routine. As a result, the pose graph has been rewritten too, now it uses the same LevMarq impl as other OpenCV functions.

Any numbers?

TLDR: new implementation converges more often in less iterations to approximately the same cost function values.

A convergence comparison across various use cases:

Convergence by function
functionold goodold totalnew goodnew total
calibrateCameraInternal15151515
estimateAffine2D11493121111217212172
solvePnPRefine5555
stereoCalibrateImpl2323
estimateAffinePartial2D4300609668586858
findExtrinsicCameraParams26018634162386329
BundleAdjusterBase::estimate783783709709
findHomography25044309573135131719
Final energy by function
functionold minnew minold maxnew maxold avgnew avgold mednew med
calibrateCameraInternal0.0083210.008317558.9558.973.6773.674.7264.726
estimateAffine2D003.267e+073.267e+077.014e+045.368e+050.0090580.009339
solvePnPRefine1.05e-262.985e-120.0049710.0049710.00099410.00099413.506e-172.179e-11
stereoCalibrateImpl6.7886.788318.8315.1110.8109.56.7886.788
estimateAffinePartial2D0012091209115.8108.46.931e-093.247e-09
findExtrinsicCameraParams21.883e-292.958e-312.729e+422.729e+424.304e+384.313e+382.898e-104.199e-10
BundleAdjusterBase::estimate26264.598e+044.598e+0418692274266.2764.6
findHomography4.645e-270230423043430.60.0038763.122e-05
Iterations elapsed till convergence by function
functionold minnew minold maxnew maxold avgnew avgold mednew med
calibrateCameraInternal541575460.0724.737026
estimateAffine2D1121310.391.83862
solvePnPRefine327453.654
stereoCalibrateImpl451711581581
estimateAffinePartial2D112164.5611.65541
findExtrinsicCameraParams21142203.0151.15821
BundleAdjusterBase::estimate19121003864.323.224554
findHomography1121107.5532.26751

Other changes in this PR

  • Stereo calibration was broken during 4.x to 5.x porting, fixing it
  • Temporary fixes forSubmap class and related stuff (anyway it'll be done using updatedPoseGraph class)
  • minor changes

Pull Request Readiness Checklist

See details athttps://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on a code under GPL or other license that is incompatible with OpenCV
  • The PR is proposed to proper branch
  • There is reference to original bug report and related work
  • There is accuracy test, performance test and test data in opencv_extra repository, if applicable
    Patch to opencv_extra has the same branch name.
  • The feature is well documented and sample code can be built with the project CMake

catree and vpisarev reacted with thumbs up emojivpisarev and bluecamel reacted with hooray emoji
@savuorsavuor changed the base branch fromnext to5.xNovember 8, 2021 10:04
@savuorsavuor marked this pull request as ready for reviewDecember 9, 2021 16:25
@savuorsavuor requested a review fromalalekDecember 10, 2021 11:19
Copy link
Member

@alalekalalek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We should revise public API.
Keep minimal part which is necessary for user application (tests).

Internal interfaces like "BaseLevMarq::Backend" should be hidden.

For more details, please refer to Wikipedia page (https://en.wikipedia.org/wiki/Levenberg%E2%80%93Marquardt_algorithm).
*/
classCV_EXPORTSLMSolver : public Algorithm
classCV_EXPORTSBaseLevMarq
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

BaseLevMarq -> LevMarqBase or LevMarqSolver

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Done


Ptr<Backend> pBackend;

BaseLevMarq(Ptr<Backend> backend_) :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Again,const reference for all input parameter.


no underscores in names if we don't need them.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

fixed

Comment on lines 545 to 577
// normalize jacobian columns for better conditioning
// slows down sparse solver, but maybe this'd be useful for some other solver
bool jacobiScaling;
// double upFactor until the probe is successful
bool upDouble;
// use stepQuality metrics for steps down
bool useStepQuality;
// clamp diagonal values added to J^T*J to pre-defined range of values
bool clampDiagonal;
// to use squared L2 norm or Inf norm for step size estimation
bool stepNormInf;
// to use relEnergyDeltaTolerance or not
bool checkRelEnergyChange;
// to use minGradientTolerance or not
bool checkMinGradient;
// to use stepNormTolerance or not
bool checkStepNorm;
// to use geodesic acceleration or not
bool geodesic;
// second directional derivative approximation step for geodesic acceleration
double hGeo;
// how much of geodesic acceleration is used
double geoScale;
// optimization stops when norm2(dx) drops below this value
double stepNormTolerance;
// optimization stops when relative energy change drops below this value
double relEnergyDeltaTolerance;
// optimization stops when max gradient value (J^T*b vector) drops below this value
double minGradientTolerance;
// optimization stops when energy drops below this value
double smallEnergyTolerance;
// optimization stops after a number of iterations performed
unsignedint maxIterations;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

No fields in public API.
This is the primary requirement of public OpenCV API.
getter/setter should be exposed instead.

Copy link
Contributor

@vpisarevvpisarevDec 15, 2021
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

can we try to utilizehttps://github.com/opencv/opencv/wiki/OE-34.-Named-Parameters? It lets to define very nice API for C++ and Python (and maybe Swift after the corresponding binding generator is revised)

Copy link
ContributorAuthor

@savuorsavuorDec 21, 2021
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Implemented it as aSettings structure

double energy;
};

classBackend
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Looks like it is not used by user code directly. Move to src or details.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

The motivation to leave theBackend in public was to let a user to construct solvers that use sparse matrices, params of SE(3) and of more exotic groups, with fixed variables and so on.
I will try to move it to details to keep this possibility.
(However the interface is very complicated for a user)

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Backend is moved to details header

Comment on lines 497 to 499
bool found;
int iters;
double energy;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

missing documentation

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Added docs forReport structure

inline Vec<T,3> DualQuat<T>::getTranslation(QuatAssumeType assumeUnit)const
{
Quat<T> trans =2.0 * (getDualPart() *getRealPart().inv(assumeUnit));
Quat<T> trans =T(2.0) * (getDualPart() *getRealPart().inv(assumeUnit));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Such changes should be backported to 4.x

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Backported to#21319 and#3137@contrib

Comment on lines 1067 to 1069
solver.maxIterations = (unsignedint)(termCrit.maxCount *2.1);
solver.stepNormTolerance = termCrit.epsilon;
solver.smallEnergyTolerance = termCrit.epsilon * termCrit.epsilon;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We should have setter with TermCriteria parameter.

maxIterations = (unsigned int)(termCrit.maxCount * 2.1);
2.1

Why?

Copy link
ContributorAuthor

@savuorsavuorDec 12, 2021
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

2.1 is a compatibility hack. Old impl counts successful iterations only, the new one counts all iterations.
The proportion between them for the same run is different per use case but in average it is 2.1:newIters = oldIters*2.1

I propose three solutions here:

  • to leave this hack as is (adding a comment about it in src)
  • to find true multiplier for each use case based on iterations elapsed statistics
  • remove the multiplier: it's 5.x now, we don't have to maintain such parameters compatibility

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I think, having such a hack is fine.

  1. If the problem solved is more or less well-defined (not ill-posed), the solver should converge faster than it will reach the maximum.
  2. If the problem solved is ill-posed then we will do at least termCrit.maxCount iterations, so this hack may affect the speed, but not the quality. But, I believe, because the new solver uses the sparse structure of matrices, it should run even faster than the previous "dense" implementation.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Multiplier removed, statistics recalculated.
RegardingTermCriteria usage: I'm against it, it has only one epsilon parameter and it's not obvious to what threshold in this LevMarq impl it corresponds to.

initialLmDownFactor(3.0)
{ }

booloperator==(const Settings& other)const
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

If users don't need this functionality, then it is better to create internal function.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Removed
(was made to compare a passed arg with default settings constant, default arg is used instead)

Settings() :
jacobiScaling(false),
upDouble(true),
useStepQuality(true),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Move ctor implementation to .cpp file.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Done


Settings&jacobiScalingS (bool v) { jacobiScaling = v;return *this; }
Settings&upDoubleS (bool v) { upDouble = v;return *this; }
Settings&useStepQualityS (bool v) { useStepQuality = v;return *this; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Why is here 'S' suffix?
Where are you find that?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

S stands for "set" but I didn't want to give them names likesetEpsilon to make method names more compact.
Since you recommended not to use underscores, I seek other ways to name them.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Decided to usesetValue names instead, looks more natural

return ok;
}

Settings&jacobiScalingS (bool v) { jacobiScaling = v;return *this; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

inline for all setters

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Done

Settings&relEnergyDeltaToleranceS(double v) { relEnergyDeltaTolerance = v;return *this; }
Settings&minGradientToleranceS (double v) { minGradientTolerance = v;return *this; }
Settings&smallEnergyToleranceS (double v) { smallEnergyTolerance = v;return *this; }
Settings&maxIterationsS (unsignedint v) { maxIterations = v;return *this; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

unsigned int

This type may be non-friendly with bindings.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Replaced by int

Comment on lines 636 to 642
/*
Defined in details header
*/
classCV_EXPORTS Backend
{
public:
virtual~Callback() {}
/**
computes error and Jacobian for the specified vector of parameters
@param param the current vector of parameters
@param err output vector of errors: err_i = actual_f_i - ideal_f_i
@param J output Jacobian: J_ij = d(err_i)/d(param_j)
when J=noArray(), it means that it does not need to be computed.
Dimensionality of error vector and param vector can be different.
The callback should explicitly allocate (with "create" method) each output array
(unless it's noArray()).
*/
virtualboolcompute(InputArray param, OutputArray err, OutputArray J)const = 0;
virtual~Backend() { }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Defined in

Is it not a definition?

Why does forward declaration not work here?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Backend class removed

The final vector of parameters (whether the algorithm converged or not) is stored at the same
vector. The method returns the number of iterations used. If it's equal to the previously specified
maxIters, there is a big chance the algorithm did not converge.
LevMarqBase(const Ptr<Backend>& backend,const Settings& settings);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Users don't really need this constructor. As they can't use it.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Fixed, see the comment below

…, no Backend class, Settings() => .cpp, Settings==() removed, Settings.set...() inlines
@savuor
Copy link
ContributorAuthor

  1. LevMarqBase was moved todetail headers
  2. LevMarqDenseLinear was replaced by a classLevMarq which takes enum args responsible for "dense" and "linear" properties respectively; other types of solvers are not implemented now and reserved for future.

@savuorsavuor requested a review fromalalekDecember 27, 2021 11:27
Copy link
Member

@alalekalalek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Looks good to me 👍

savuor reacted with thumbs up emoji
@alalekalalek merged commit9d6f388 intoopencv:5.xDec 27, 2021
@savuorsavuor deleted the levmarqfromscratch branchDecember 27, 2021 21:57
@mshabuninmshabunin mentioned this pull requestJun 12, 2024
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@vpisarevvpisarevAwaiting requested review from vpisarev

@alalekalalekAwaiting requested review from alalek

Assignees

No one assigned

Projects

None yet

Milestone

5.0-alpha

Development

Successfully merging this pull request may close these issues.

3 participants

@savuor@vpisarev@alalek

[8]ページ先頭

©2009-2025 Movatter.jp