kornia/kornia-rs

[Bug]: negative robust_scale_sq yields NaN-parameterised robust loss in refine_pose_lm

Open

#1,117 opened on Aug 19, 2026

 (1 comment) (0 reactions) (0 assignees)Rust (188 forks)auto 404
bugcrate: pnpgood first issuetriage

Repository metrics

Stars
 (675 stars)
PR merge metrics
 (Avg merge 3d 9h) (39 merged PRs in 30d)

Description

🐛 Describe the bug

build_robust_loss in crates/kornia-3d/src/pnp/refine.rs (added in #1111) guards only against non-finite robust_scale_sq, not against negative values. A negative-but-finite scale produces a HuberLoss/CauchyLoss with a NaN parameter, which silently poisons the normal equations for every factor in the solve.

if params.robust == RobustKernelKind::Identity || !params.robust_scale_sq.is_finite() {
    return Ok(None);
}
let delta = params.robust_scale_sq.sqrt();

With robust_scale_sq = -25.0:

  1. is_finite() is true, so the early return is skipped.
  2. delta = (-25.0f32).sqrt() = NaN.
  3. HuberLoss::new(delta) rejects only delta <= 0.0, and NaN <= 0.0 is false — so it returns Ok(HuberLoss { delta: NaN }).
  4. HuberLoss::weight() takes the else branch (squared_norm <= NaN is false) and returns self.delta / squared_norm.sqrt() = NaN.
  5. linear_system.rs scales every residual and Jacobian block by sqrt(weight) = NaN, so J^T J and J^T r are entirely NaN and the pose result is garbage — with no error returned to the caller.

crates/kornia-3d/src/ba.rs already gets this right and checks both conditions:

if !params.robust_scale_sq.is_finite() || params.robust_scale_sq <= 0.0 {
    return None;
}

🔄 Steps to Reproduce

1. Call refine_pose_lm with LMRefineParams { robust: RobustKernelKind::Huber, robust_scale_sq: -25.0, ..Default::default() }
2. Observe the returned rotation/translation contain NaN, with Ok(_) status

💻 Minimal Code Example

let params = LMRefineParams {
    robust: RobustKernelKind::Huber,
    robust_scale_sq: -25.0, // negative, finite -> NaN delta
    ..LMRefineParams::default()
};
let result = refine_pose_lm(&pts_w, &pts_i, &k, &r_init, &t_init, None, &params)?;
assert!(result.translation.x.is_nan()); // currently passes

✅ Expected behaviour

A non-positive robust_scale_sq should either fall back to plain L2 (matching ba.rs) or be reported as an invalid-parameter error. It should never produce a NaN-parameterised loss.

🔧 Suggested fix

One extra clause, mirroring ba.rs (NaN is already covered by is_finite):

if params.robust == RobustKernelKind::Identity
    || !params.robust_scale_sq.is_finite()
    || params.robust_scale_sq <= 0.0
{
    return Ok(None);
}

Worth a regression test asserting a finite result for negative/zero scales, and ideally the same guard audited in ba_schur.rs and pose/lm_pose.rs.

📎 Context

Introduced by #1111 (Huber PnP refine).

Contributor guide