Problem

Problem Introduction

class Problem

Problem 内保管一个带有边界约束的非线性最小二乘问题,通过使用 Problem::AddResidualBlockProblem::AddParameterBlock 来建立这个问题。例如一个非线性优化问题包含三个参数块,每个参数块的大小分别为 3,4,5。同时拥有两个残差块,残差的维度分别为 2 和 6。定义方式如下:

double x1[] = { 1.0, 2.0, 3.0 };
double x2[] = { 1.0, 2.0, 3.0, 5.0 };
double x3[] = { 1.0, 2.0, 3.0, 6.0, 7.0 };

Problem problem;
problem.AddResidualBlock(new MyUnaryCostFunction(...), x1);
problem.AddResidualBlock(new MyBinaryCostFunction(...), x2, x3);

Problem::AddParameterBlock 正如其函数名一样,是将一个残差块添加到优化问题中去,它需要几个参数,分别是 CostFunction,一个可选的 LossFunction,以及 CostFunction 进行计算所需要的参数块。

CostFunction 内部存储了它所期望的参数块大小的信息。函数会检查这些信息是否与 parameter_blocks 中列出的参数块大小一致。loss_function 可以是 nullptr,在这种情况下,每一项的 cost 只是残差的平方。用户可以选择使用 Problem::AddParameterBlock() 明确添加参数块。不过,如果参数块不存在,Problem::AddResidualBlock() 会隐式添加参数块,因此不需要明确调用 Problem::AddParameterBlock()

所以说在很多实际应用中你会发现很多优化问题在定义的时候是没有调用过 Problem::AddParameterBlock() 方法的,即使是在一些需要特殊流形(例如四元数)参与的情况下,用户也可以在添加完残差块后,再对特定的参数块设置流形即可。

Problem::AddParameterBlock() 明确地将参数块添加到问题中。用户也可以选择将 Manifold 对象与参数块关联起来。相同参数重复调用该方法将被忽略,但若是在调用时发现两次的参数块大小设置不一致则会出现警告。

您可以使用 Problem::SetParameterBlockConstant() 将任何参数块设置为常量,也可以使用 SetParameterBlockVariable() 撤销设置。事实上,你可以设置任意数量的参数块为常数,而 Ceres 能找出你所构建问题的哪一部分取决于可自由变化的参数块,并只处理该部分。因此,举例来说,如果你构建了一个有 100 万个参数块和 200 万个残差块的问题,但除了 1 个参数块外,其他所有参数块都设置为常数,并且只有 10 个残差块取决于这一个非常数参数块。那么,Ceres 在解决这个问题上所花费的计算时间,将与你定义一个有 1 个参数块和 10 个残差块的问题所花费的计算时间相同。

默认情况下,Problem 拥有 cost_functionloss_functionmanifold 指针的所有权。这些对象在 Problem 的生命周期内保持有效。如果用户希望控制这些对象的销毁,可以通过在 Problem::Options 结构中设置相应的参数来实现。

请注意,尽管 Problem 拥有 cost_functionloss_function 对象的所有权,但并不妨碍用户在另一个残差块中重新使用它们。同样,同一个流形对象也可以用于多个参数块。析构函数会对每个拥有的对象做出精确的删除。

Member Variable and Methods

class Problem::Options

用户控制 Problem 的相关选项


Ownership Problem::Options::cost_function_ownership
  • default : TAKE_OWNERSHIP

此选项控制 Problem 对象是否拥有代价函数。如果设置为 TAKE_OWNERSHIP,那么 Problem 对象将在销毁时删除代价函数。由于允许共享代价函数,因此析构函数会只删除一次指针。


Ownership Problem::Options::loss_function_ownership
  • Default : TAKE_OWNERSHIP

此选项控制 Problem 对象是否拥有鲁棒核函数。如果设置为 TAKE_OWNERSHIP,那么 Problem 对象将在销毁时删除鲁棒核函数。由于允许共享鲁棒核函数,因此析构函数会只删除一次指针。


Ownership Problem::Options::manifold_ownership
  • Default : TAKE_OWNERSHIP

此选项控制 Problem 对象是否拥有 Manifolds。如果设置为 TAKE_OWNERSHIP,那么 Problem 对象将在销毁时删除Manifolds。由于允许共享Manifolds,因此析构函数会只删除一次指针。


bool Problem::Options::enable_fast_removal
  • Default : false

如果为 true,则会用哈希表来存储参数块(残差块)和原始指针的对应关系,以加快 Problem::RemoveResidualBlock()Problem::RemoveParameterBlock() 操作的速度,同样的内存占用也会增加。默认情况下,Problem::RemoveParameterBlock()Problem::RemoveResidualBlock() 所耗费的时间与整个问题的大小成正比。如果用户只是偶尔从问题中删除参数或残差,这点时间耗费可能是可以接受的。但是,如果有空余内存,可以启用此选项,使 Problem::RemoveParameterBlock() 的耗时与依赖它的残差块的数量成正比,而 Problem::RemoveResidualBlock() 的耗时(平均)不变。

内存使用量会增加两倍:每个参数块都会增加一个哈希集,其中包含所有依赖于该参数块的残差;问题中的哈希集包含所有残差。


bool Problem::Options::disable_all_safety_checks
  • Default : false

默认情况下,Ceres 会在构建问题时执行各种安全检查。这些检查会带来微小的性能损失,通常约占构建时间的 5%。如果您确信您的问题构造是正确的,而且您确实希望避免这 5%的问题构造时间,那么您可以将 disable_all_safety_checks 设置为 true

Do not set this to true, unless you are absolutely sure of what you are doing.


Context *Problem::Options::context
  • Default : nullptr

A Ceres global context to use for solving this problem. This may help to reduce computation time as Ceres can reuse expensive objects to create. The context object can be nullptr, in which case Ceres may create one。Ceres 不拥有指针的所有权。


EvaluationCallback *Problem::Options::evaluation_callback
  • Default : nullptr

Using this callback interface, Ceres will notify you when it is about to evaluate the residuals or Jacobians.

If an evaluation_callback is present, Ceres will update the user’s parameter blocks to the values that will be used when calling CostFunction::Evaluate() before calling EvaluationCallback::PrepareForEvaluation(). One can then use this callback to share (or cache) computation between cost functions by doing the shared computation in EvaluationCallback::PrepareForEvaluation() before Ceres calls CostFunction::Evaluate().

Problem does NOT take ownership of the callback.

Evaluation callbacks are incompatible with inner iterations. So calling Solve with Solver::Options::use_inner_iterations set to true on a Problem with a non-null evaluation callback is an error.

ResidualBlockId Problem::AddResidualBlock(CostFunction *cost_function, LossFunction *loss_function, const std::vector<double*> parameter_blocks)
template<typename Ts...>
ResidualBlockId Problem::AddResidualBlock(CostFunction *cost_function, LossFunction *loss_function, double *x0, Ts... xs)

在总成本函数中添加一个残差块。cost_function 携带了它所期望的参数块大小的信息。函数会检查这些信息是否与 parameter_blocks 中列出的参数块大小一致。loss_function 可以是 nullptr,在这种情况下,该项的代价只是残差的平方。

参数块可以以 vector<double*>double* 指针的形式一起传递。用户可以选择使用 AddParameterBlock 明确添加参数块。这将导致额外的合法性检查;不过,如果没有参数块,AddResidualBlock 会隐式添加参数块,因此不需要显式调用 AddParameterBlock

Problem 对象默认拥有 cost_functionloss_function 指针。这些对象在 Problem 对象的生命周期内保持有效。如果用户希望控制这些对象的销毁,可以通过在 Options 结构中设置相应的参数来实现。

Even though the Problem takes ownership of cost_function and loss_function, it does not preclude the user from re-using them in another residual block. The destructor takes care to call delete on each cost_function or loss_function pointer only once, regardless of how many residual blocks refer to them.

Example usage:

double x1[] = {1.0, 2.0, 3.0};
double x2[] = {1.0, 2.0, 5.0, 6.0};
double x3[] = {3.0, 6.0, 2.0, 5.0, 1.0};
std::vector<double*> v1;
v1.push_back(x1);
std::vector<double*> v2;
v2.push_back(x2);
v2.push_back(x1);

Problem problem;

problem.AddResidualBlock(new MyUnaryCostFunction(...), nullptr, x1);
problem.AddResidualBlock(new MyBinaryCostFunction(...), nullptr, x2, x1);
problem.AddResidualBlock(new MyUnaryCostFunction(...), nullptr, v1);
problem.AddResidualBlock(new MyBinaryCostFunction(...), nullptr, v2);

void Problem::AddParameterBlock(double *values, int size, Manifold *manifold)

在问题中添加指定大小的参数块和 Manifold。manifold 可以是 nullptr

相同参数的重复调用将被 Ceres 忽略。但若重复调用时设置了和先前不同的参数块 size 则会导致崩溃(除非 Solver::Options::disable_all_safety_checks 设置为 true)。若重复调用,且大小相同,但 manifold 不同,此时相当于调用 SetManifold(),即之前关联的 manifold 对象将被替换。


void Problem::AddParameterBlock(double *values, int size)

在问题中添加指定大小的参数块。相同参数的重复调用将被 Ceres 忽略。但若重复调用时设置了和先前不同的参数块 size 则会导致崩溃(除非 Solver::Options::disable_all_safety_checks 设置为 true)。


void Problem::RemoveResidualBlock(ResidualBlockId residual_block)

从问题中移除一个 residual block。由于残差块可以共享成本函数和损失函数对象,因此 Ceres Solver 使用引用计数机制。当删除残差块时,相应成本函数和损失函数对象的引用计数就会减少,当该计数达到零时,它们就会被删除。如果 Problem::Options::enable_fast_removal 为 true,则删除速度很快(几乎是常数复杂度)。否则它是线性的,需要遍历所有残差块。删除残差块对问题所依赖的参数块没有影响。

Removing a residual or parameter block will destroy the implicit ordering, rendering the jacobian or residuals returned from the solver uninterpretable. If you depend on the evaluated jacobian, do not use remove! This may change in a future release. Hold the indicated parameter block constant during optimization.


void Problem::RemoveParameterBlock(const double *values)

从问题中删除参数块。依赖于该参数的任何 residual block 也会被删除,如上面在RemoveResidualBlock() 中所述。如果该参数块有对应的流形,流形对象将持续存在,直到问题被删除。如果 Problem::Options::enable_fast_removal 为 true,则删除速度很快(几乎是常数复杂度)。否则,删除参数块将遍历整个问题。

Removing a residual or parameter block will destroy the implicit ordering, rendering the jacobian or residuals returned from the solver uninterpretable. If you depend on the evaluated jacobian, do not use remove! This may change in a future release.


void Problem::SetParameterBlockConstant(const double *values)

在优化期间让指定的参数块为恒定值,该参数块不进行优化。


void Problem::SetParameterBlockVariable(double *values)

允许指示的参数在优化期间发生变化。


bool Problem::IsParameterBlockConstant(const double *values) const

如果参数块设置为常量,则返回 true,否则返回 false。参数块可以通过两种方式设置常量:通过调用 SetParameterBlockConstant 或通过将 Manifold 与零维切空间相关联。


void SetManifold(double *values, Manifold *manifold);

设置参数块的 Manifold。使用 nullptr 调用 Problem::SetManifold() 将清除之前为参数块设置的任何 Manifold。重复调用将导致任何先前关联的 Manifold 对象被替换为新的 Manifold。默认情况下,流形由 Problem 所有(请参阅 Problem::Options )。可以为多个参数块设置相同的 Manifold


const Manifold *GetManifold(const double *values) const;

获取与此参数块关联的 Manifold 对象。如果没有与参数块关联的 Manifold 对象,则返回 nullptr


bool HasManifold(const double *values) const;

如果 Manifold 与此参数块关联,则返回 true,否则返回 false。


void Problem::SetParameterLowerBound(double *values, 
                                     int index, 
                                     double lower_bound)

设置参数块中对应位置处的参数值下限。默认情况下,下限为 -std::numeric_limits::max(),求解器将其视为与负无穷大。


void Problem::SetParameterUpperBound(double *values, 
                                     int index, 
                                     double upper_bound)

设置参数块中对应位置处的参数值上限。默认情况下,该值为 std::numeric_limits::max(),求解器将其视为正无穷大。


double Problem::GetParameterLowerBound(const double *values, int index)

获取对应位置的参数值的下限。如果用户没有限制,则其下限为 -std::numeric_limits::max()


double Problem::GetParameterUpperBound(const double *values, int index)

获取对应位置的参数值的上限。如果用户没有限制,则其上限为 std::numeric_limits::max()


int Problem::NumParameterBlocks() const

问题中的参数块数量。始终等于 parameter_blocks().size()parameter_block_sizes().size()


int Problem::NumParameters() const

通过对所有参数块的大小求和获得的参数向量的大小。


int Problem::NumResidualBlocks() const

问题中的残差块数量。始终等于 residual_blocks().size()


int Problem::NumResiduals() const

通过对所有残差块的大小求和而获得的残差向量的大小。


int Problem::ParameterBlockTangentSize(const double *values) const

参数块的流形切线空间的尺寸。如果没有与此参数块关联的 Manifold,则 ParameterBlockTangentSize = ParameterBlockSize


bool Problem::HasParameterBlock(const double *values) const

判断问题中是否存在给定的参数块。


void Problem::GetParameterBlocks(
    std::vector<double*> *parameter_blocks) const

获取当前问题中的参数块,用指向当前问题中的参数块的指针填充 parameter_blocks 向量。调用后,parameter_block.size() == NumParameterBlocks


void Problem::GetResidualBlocks(
    std::vector<ResidualBlockId> *residual_blocks) const

获取当前问题中的残差块,用指向当前问题中的残差块的指针填充残差块向量 residual_blocks。调用后,residual_blocks.size() == NumResidualBlocks


void Problem::GetParameterBlocksForResidualBlock(
    const ResidualBlockId residual_block, 
    std::vector<double*> *parameter_blocks) const

获取依赖于给定残差块的所有参数块。


void Problem::GetResidualBlocksForParameterBlock(
    const double *values, 
    std::vector<ResidualBlockId> *residual_blocks) const

获取依赖于给定参数块的所有残差块。如果 Problem::Options::enable_fast_removaltrue,则获取残差块的速度很快,并且仅取决于残差块的数量。否则,获取参数块的残差块将扫描整个问题。


const CostFunction *Problem::GetCostFunctionForResidualBlock(
    const ResidualBlockId residual_block) const

获取给定残差块的 CostFunction


const LossFunction *Problem::GetLossFunctionForResidualBlock(
    const ResidualBlockId residual_block) const

获取给定残差块的 LossFunction


bool EvaluateResidualBlock(
    ResidualBlockId residual_block_id, 
    bool apply_loss_function, 
    double *cost, 
    double *residuals, 
    double **jacobians) const

评估残差块,将标量残差存储在 cost 中,将残差分量存储在 residuals 中,并按行优先顺序将参数和残差之间的 jacobian 矩阵存储在 jacobians[i] 中。如果 residualsnullptrresiduals 将不会被计算。如果 jacobiansnullptrjacobains 不会被计算,如果 jacobians[i] 是空的,则对于该参数块的 jacobian 也不会被计算,例如当参数块为常数的时候。返回值代表计算是否成功,即使返回的是 false,用户也应当意识到相应内存中的值也可能已经发生了变化。返回的残差和 jacobians 已经利用了鲁棒核函数和流形,例如,使用 QuaternionManifold 的四元数参数的雅可比矩阵是 num_residuals x 3,而不是 num_residuals x 4apply_loss_function 顾名思义,允许用户打开和关闭损失函数。

If an EvaluationCallback is associated with the problem, then its EvaluationCallback::PrepareForEvaluation() method will be called every time this method is called with new_point = true. This conservatively assumes that the user may have changed the parameter values since the previous call to evaluate / solve. For improved efficiency, and only if you know that the parameter values have not changed between calls, see Problem::EvaluateResidualBlockAssumingParametersUnchanged().


bool EvaluateResidualBlockAssumingParametersUnchanged(
    ResidualBlockId residual_block_id, 
    bool apply_loss_function, 
    double *cost, double *residuals, 
    double **jacobians) const

Same as Problem::EvaluateResidualBlock() except that if an EvaluationCallback is associated with the problem, then its EvaluationCallback::PrepareForEvaluation() method will be called every time this method is called with new_point = false.

This means, if an EvaluationCallback is associated with the problem then it is the user’s responsibility to call EvaluationCallback::PrepareForEvaluation() before calling this method if necessary, i.e. iff the parameter values have been changed since the last call to evaluate / solve.’

This is because, as the name implies, we assume that the parameter blocks did not change since the last time EvaluationCallback::PrepareForEvaluation() was called (via Solve(), Problem::Evaluate() or Problem::EvaluateResidualBlock()).


bool Problem::Evaluate(
    const Problem::EvaluateOptions &options,
    double *cost, std::vector<double> *residuals,
    std::vector<double> *gradient, 
    CRSMatrix *jacobian)

评估 Problem。任何输出指针都可以为 nullptr。使用哪些残差块和参数块由下面的 Problem::EvaluateOptions 控制。Evaluate 将使用在构建问题时参数块指针所指向的内存位置中的值,例如在以下代码中:

Problem problem;
double x = 1;
problem.Add(new MyCostFunction, nullptr, &x);

double cost = 0.0;
problem.Evaluate(Problem::EvaluateOptions(), 
                 &cost, nullptr, nullptr, nullptr);

计算在 x=1x = 1 时的残差。如果您希望评估在 x=2x = 2 时的残差大小,则

x = 2;
problem.Evaluate(Problem::EvaluateOptions(),
                 &cost, nullptr, nullptr, nullptr);

If no Manifoldare used, then the size of the gradient vector is the sum of the sizes of all the parameter blocks. If a parameter block has a manifold then it contributes “TangentSize” entries to the gradient vector.

This function cannot be called while the problem is being solved, for example it cannot be called from an IterationCallback at the end of an iteration during a solve.

If an EvaluationCallback is associated with the problem, then its PrepareForEvaluation method will be called everytime this method is called with new_point = true.


class Problem::EvaluateOptions

用于控制 Problem::Evaluate() 的配置参数。


std::vector<double*> Problem::EvaluateOptions::parameter_blocks

应进行评估的参数块集合。parameter_blocks 决定了参数块在梯度向量和 Jacobian 矩阵中出现的顺序。如果 parameter_blocks 为空,则假定它为包含了 Problem 所有参数块的向量。一般来说,这种情况下参数块的排序取决于它们被添加到问题中的顺序,以及用户是否删除了一些参数块。注意:该向量所包含的指针应与向问题中添加参数块的指针相同。这些参数块不应指向新的内存位置。


std::vector<ResidualBlockId> Problem::EvaluateOptions::residual_blocks

应进行评估的残差块集合。residual_blocks 向量决定了残差出现的顺序,以及 jacobian 行的排序方式。如果 residual_blocks 为空,则假定它为包含所有残差块的向量。


bool Problem::EvaluateOptions::apply_loss_function

即使问题中的残差块可能包含损失函数,将 apply_loss_function 设置为 false 也会关闭损失函数。例如,如果用户希望通过研究求解前后的残差来分析求解质量,就可以使用这种方法。


int Problem::EvaluateOptions::num_threads

使用的线程数。

Last updated