Chapter 12: SLAM II — Graph-Based SLAM
Lesson 4: Handling Outliers (robust kernels)
Graph-based SLAM reduces localization + mapping to sparse nonlinear least-squares over robot poses (and optionally landmarks). In real deployments, a small fraction of constraints can be grossly wrong (false loop closures, dynamic objects, perceptual aliasing). This lesson develops robust-kernel (M-estimation) techniques that softly downweight such outliers while preserving the Gauss–Newton / Levenberg–Marquardt structure from Lesson 3.
1. Why Outliers Matter in Graph-Based SLAM
In a pose graph, each edge encodes a relative measurement between two poses. Standard optimization assumes near-Gaussian noise. Outliers violate this assumption: their errors have heavy tails or are multi-modal, so a sum-of-squared residuals objective can be dominated by a few wrong constraints.
Let node \( i \) have pose \( \mathbf{x}_i \), and edge \( (i,j) \) provide measurement \( \mathbf{z}_{ij} \). The edge defines a residual \( \mathbf{r}_{ij}(\mathbf{x}_i,\mathbf{x}_j) \) which should be near zero for a correct estimate. A single false loop closure can “pull” the whole solution away from the trajectory supported by the majority of edges.
Robust kernels fix this by reducing the influence of large residuals without brittle hard rejection (which can fail early in optimization when residuals are temporarily large).
2. Recap: Standard Pose-Graph Least Squares
For each edge, define the squared Mahalanobis error \( s_{ij} \):
\[ s_{ij}(\mathbf{x}) = \mathbf{r}_{ij}(\mathbf{x})^{\top}\,\mathbf{\Omega}_{ij}\,\mathbf{r}_{ij}(\mathbf{x}), \qquad \mathbf{\Omega}_{ij}=\mathbf{\Sigma}_{ij}^{-1}. \]
The classical objective minimizes \( F(\mathbf{x})=\sum_{(i,j)\in\mathcal{E}} s_{ij}(\mathbf{x}) \). Linearizing at \( \mathbf{x}^{(k)} \) yields \( \mathbf{r}_{ij}(\mathbf{x}^{(k)}\oplus\Delta\mathbf{x}) \approx \mathbf{r}_{ij} + \mathbf{J}_{ij}\Delta\mathbf{x} \), leading to sparse normal equations \( \mathbf{H}\Delta\mathbf{x}=-\mathbf{g} \) (Gauss–Newton) or damped (LM).
The weakness: \( s_{ij} \) grows quadratically with residual magnitude, so an outlier can dominate both \( \mathbf{H} \) and \( \mathbf{g} \).
3. Robust Kernels as M-Estimators
Robust estimation replaces the quadratic penalty on \( s \) by a sub-quadratic function \( \rho(s) \) (a robust kernel):
\[ \min_{\mathbf{x}}\;F_{\rho}(\mathbf{x}) \;=\;\sum_{(i,j)\in\mathcal{E}} \rho\!\left(s_{ij}(\mathbf{x})\right). \]
3.1 Huber kernel (convex “soft clipping”)
A common convention applies the kernel to \( \sqrt{s}=\|\mathbf{r}\|_{\Omega} \):
\[ \rho_{\text{Huber}}(s)= \begin{cases} s, & \sqrt{s}\le \delta \\ 2\delta\sqrt{s}-\delta^2, & \sqrt{s} \gt \delta \end{cases} \]
For small errors it matches least squares; beyond \( \delta \) it grows linearly in \( \sqrt{s} \).
3.2 Redescending kernels (aggressive suppression)
Kernels like Tukey’s biweight can have redescending influence: extremely large residuals receive near-zero weight. This is powerful but introduces non-convexity and additional local minima. In mobile-robot SLAM, Huber is often a safe first choice.
4. Influence Function and “Bounded Influence”
For a scalar residual \( r \) with penalty \( \phi(r) \), define the influence function \( \psi(r)=\frac{d}{dr}\phi(r) \). Least squares uses \( \phi(r)=r^2 \), hence \( \psi(r)=2r \) (unbounded as \( |r|\to\infty \)).
Robust kernels bound or slow the growth of \( \psi(r) \). For the scalar Huber penalty:
\[ \phi_{\text{Huber}}(r)= \begin{cases} r^2, & |r|\le \delta \\ 2\delta|r|-\delta^2, & |r| \gt \delta \end{cases} \quad\Longrightarrow\quad \psi_{\text{Huber}}(r)= \begin{cases} 2r, & |r|\le \delta \\ 2\delta\,\operatorname{sign}(r), & |r| \gt \delta. \end{cases} \]
Thus a single huge outlier cannot create arbitrarily large force on the optimizer—precisely what we need when false loop closures exist.
5. MAP Interpretation: Heavy-Tailed Likelihoods
Robust kernels correspond to heavy-tailed noise models. Assume independent edges with likelihood \( p(\mathbf{z}_{ij}\mid\mathbf{x})\propto \exp\!\big(-\tfrac{1}{2}\rho(s_{ij}(\mathbf{x}))\big) \). Then the MAP estimate is:
\[ \mathbf{x}_{\text{MAP}} =\arg\max_{\mathbf{x}}\prod_{(i,j)\in\mathcal{E}}p(\mathbf{z}_{ij}\mid\mathbf{x}) =\arg\min_{\mathbf{x}}\sum_{(i,j)\in\mathcal{E}}\rho\!\left(s_{ij}(\mathbf{x})\right). \]
Example (Cauchy-style). If a scalar residual has density \( p(r)\propto\frac{1}{1+(r/c)^2} \), then the negative log-likelihood is \( \phi(r)=\log\!\left(1+(r/c)^2\right) \), which grows only logarithmically for large \( |r| \).
6. IRLS: Solving Robust SLAM via Weighted Least Squares
We minimize \( \sum_i \rho(s_i) \), where \( s_i=\mathbf{r}_i^{\top}\Omega_i\mathbf{r}_i \). Linearize \( \mathbf{r}_i(\mathbf{x}^{(k)}\oplus\Delta\mathbf{x})\approx \mathbf{r}_i+\mathbf{J}_i\Delta\mathbf{x} \).
6.1 IRLS surrogate and weight
A standard IRLS step freezes a scalar weight computed from current residuals and solves:
\[ \Delta\mathbf{x}^{(k)} =\arg\min_{\Delta\mathbf{x}} \sum_i w_i\,\left\|\mathbf{r}_i+\mathbf{J}_i\Delta\mathbf{x}\right\|_{\Omega_i}^2, \qquad w_i=\rho'(s_i^{(k)}). \]
This reuses the same sparse solver as least squares, but with scaled information \( \Omega_i \leftarrow w_i\Omega_i \).
6.2 Huber weight (explicit)
\[ w(s)=\rho'(s)= \begin{cases} 1, & \sqrt{s}\le \delta \\ \delta/\sqrt{s}, & \sqrt{s} \gt \delta. \end{cases} \]
flowchart TD
A["Inputs: current poses x, edges (i,j,z,Omega), kernel params"] --> B["For each edge: compute residual r and scalar s = r^T Omega r"]
B --> C["Compute weight w from s (e.g., Huber w=1 or w=delta/sqrt(s))"]
C --> D["Linearize: r(x + dx) approx r + J dx (numeric or analytic Jacobians)"]
D --> E["Assemble normal equations with scaled info: Omega <- w*Omega"]
E --> F["Solve (H + lambda I) dx = -g"]
F --> G["Update poses; repeat until ||dx|| small"]
Stability note. Robust kernels can introduce non-convexity (especially redescending kernels). Practical strategy: start with a mild kernel (Huber) and use damping (LM) or continuation (start with large \( \delta \), decrease).
7. Kernel Choices and Tuning Guidelines
Common robust kernels in SLAM back-ends include Huber, Cauchy, Geman–McClure, and Tukey. Huber is convex and typically robust enough for occasional wrong loop closures; redescending kernels are stronger but more non-convex.
7.1 Scale selection using inlier statistics
Let residual dimension be \( d \) (e.g., \( d=3 \) for 2D relative pose). Under the correct Gaussian inlier model, \( s \) is approximately chi-square: \( s\sim\chi^2_d \). Choose \( \delta \) so that inliers stay in the quadratic region with probability \( p \):
\[ \mathbb{P}(s\le \delta^2)=p \quad\Longrightarrow\quad \delta=\sqrt{\chi^2_{d,p}}. \]
7.2 Library mapping (what you see in real SLAM stacks)
- g2o: per-edge robust kernel objects (Huber/Cauchy/etc.) that scale information matrices during optimization.
-
Ceres Solver:
LossFunction(HuberLoss, CauchyLoss, …) attached to residual blocks. - GTSAM: robust noise models (m-estimators) wrapping Gaussian factors.
8. Robust Back-End Workflow in a Mobile Robot
Robust kernels are a back-end defense layer: they do not repair perception, but they prevent a small number of bad constraints from corrupting the global estimate.
flowchart TD
S["Sensors (LiDAR / vision / wheel / IMU)"] --> FE["Front-end: feature or scan matching"]
FE --> ED["Create constraints: odometry edges + loop closures"]
ED --> QC["Basic checks: time sync, covariance sanity, optional gating"]
QC --> OPT["Back-end: nonlinear least squares with robust kernels"]
OPT --> OUT["Output: optimized trajectory + quality metrics"]
9. Python Lab — IRLS with Huber Kernel (from scratch)
The following implementation uses numeric Jacobians (finite differences) for clarity and includes one incorrect loop closure to illustrate robust downweighting. It solves twice: (i) plain least squares and (ii) Huber IRLS.
Rendering note: inside HTML <code> blocks,
characters like < / > are
escaped for safe rendering. The downloadable files contain executable
code.
Chapter12_Lesson4.py
#!/usr/bin/env python3
# Chapter12_Lesson4.py
# Graph-Based SLAM (2D Pose Graph) with Robust Kernels via IRLS (Huber)
# Minimal from-scratch educational implementation (numeric Jacobians).
import numpy as np
def wrap_angle(a):
return (a + np.pi) % (2.0 * np.pi) - np.pi
def between(xi, xj):
"""Relative pose z = xi^{-1} ∘ xj expressed in i-frame.
xi, xj: [x, y, theta]
"""
dx = xj[0] - xi[0]
dy = xj[1] - xi[1]
c = np.cos(xi[2]); s = np.sin(xi[2])
xr = c * dx + s * dy
yr = -s * dx + c * dy
tr = wrap_angle(xj[2] - xi[2])
return np.array([xr, yr, tr], dtype=float)
def huber_weight_from_s(s, delta):
"""Huber on sqrt(s): rho(s)=s if sqrt(s)<=delta else 2*delta*sqrt(s)-delta^2.
IRLS weight w = rho'(s) = 1 if sqrt(s)<=delta else delta/sqrt(s).
"""
r = np.sqrt(max(s, 1e-12))
return 1.0 if r <= delta else (delta / r)
def numeric_jacobian_edge(xi, xj, z, eps=1e-6):
"""Return Jacobians of error e wrt xi and xj (3x3 each) via finite differences."""
def err(a, b):
zhat = between(a, b)
e = zhat - z
e[2] = wrap_angle(e[2])
return e
e0 = err(xi, xj)
Ji = np.zeros((3, 3))
Jj = np.zeros((3, 3))
for k in range(3):
d = np.zeros(3); d[k] = eps
ei = err(xi + d, xj)
Ji[:, k] = (ei - e0) / eps
ej = err(xi, xj + d)
Jj[:, k] = (ej - e0) / eps
return e0, Ji, Jj
def solve_pose_graph_irls(
x_init, edges,
robust="huber", delta=1.5,
n_iters=15, damping=1e-6,
fix_first=True
):
"""Gauss-Newton / IRLS for 2D pose graph.
x_init: (N,3)
edges: list of dicts {i,j,z(3,),Omega(3x3)}
robust: "none" or "huber"
"""
x = x_init.copy()
N = x.shape[0]
# Variable ordering: optionally fix pose 0 to remove gauge freedom
if fix_first:
var_nodes = list(range(1, N))
base = 1
else:
var_nodes = list(range(N))
base = 0
dim = 3 * len(var_nodes)
node_to_col = {nid: 3 * idx for idx, nid in enumerate(var_nodes)}
for it in range(n_iters):
H = np.zeros((dim, dim))
b = np.zeros((dim,))
total_rho = 0.0
total_s = 0.0
for e in edges:
i, j = e["i"], e["j"]
z = e["z"]
Omega = e["Omega"]
ei, Ji, Jj = numeric_jacobian_edge(x[i], x[j], z)
s = float(ei.T @ Omega @ ei)
total_s += s
if robust == "huber":
w = huber_weight_from_s(s, delta)
# robust cost value (for monitoring)
r = np.sqrt(max(s, 1e-12))
if r <= delta:
rho = s
else:
rho = 2.0 * delta * r - delta * delta
total_rho += rho
else:
w = 1.0
total_rho += s
W = w * Omega
# Assemble into normal equations, skipping fixed node
def add_block(a, Ja, bnode, Jb):
Ha = Ja.T @ W @ Ja
Hab = Ja.T @ W @ Jb
ga = Ja.T @ W @ ei
ca = node_to_col.get(a, None)
cb = node_to_col.get(bnode, None)
if ca is not None:
H[ca:ca+3, ca:ca+3] += Ha
b[ca:ca+3] += ga
if ca is not None and cb is not None:
H[ca:ca+3, cb:cb+3] += Hab
# NOTE: symmetric part is added when edge is processed; we'll enforce symmetry later.
# i block
add_block(i, Ji, j, Jj)
# j block
add_block(j, Jj, i, Ji) # adds Hj and Hji, gj
# Symmetrize, add damping (Levenberg-style)
H = 0.5 * (H + H.T)
H += damping * np.eye(dim)
# Solve for dx: H dx = -b
try:
dx = -np.linalg.solve(H, b)
except np.linalg.LinAlgError:
dx = -np.linalg.lstsq(H, b, rcond=None)[0]
# Apply update
for nid in var_nodes:
c = node_to_col[nid]
x[nid, 0] += dx[c + 0]
x[nid, 1] += dx[c + 1]
x[nid, 2] = wrap_angle(x[nid, 2] + dx[c + 2])
print(f"iter {it:02d} sum_s={total_s:.3f} sum_rho={total_rho:.3f} |dx|={np.linalg.norm(dx):.3e}")
if np.linalg.norm(dx) < 1e-8:
break
return x
def make_synthetic_graph(N=20, sigma_xy=0.05, sigma_th=0.02, seed=0):
rng = np.random.default_rng(seed)
# Ground truth: a smooth arc
gt = np.zeros((N, 3))
for k in range(1, N):
gt[k, 0] = gt[k-1, 0] + 0.3 * np.cos(gt[k-1, 2])
gt[k, 1] = gt[k-1, 1] + 0.3 * np.sin(gt[k-1, 2])
gt[k, 2] = wrap_angle(gt[k-1, 2] + 0.05)
# Odometry edges (chain)
Omega = np.diag([1.0/(sigma_xy**2), 1.0/(sigma_xy**2), 1.0/(sigma_th**2)])
edges = []
for k in range(N-1):
z = between(gt[k], gt[k+1])
z_noisy = z + rng.normal([0,0,0], [sigma_xy, sigma_xy, sigma_th])
z_noisy[2] = wrap_angle(z_noisy[2])
edges.append({"i": k, "j": k+1, "z": z_noisy, "Omega": Omega})
# One correct loop closure (optional)
z_loop = between(gt[3], gt[N-2]) + rng.normal([0,0,0], [sigma_xy, sigma_xy, sigma_th])
z_loop[2] = wrap_angle(z_loop[2])
edges.append({"i": 3, "j": N-2, "z": z_loop, "Omega": Omega})
# One outlier loop closure
z_bad = np.array([2.0, -1.0, 1.0]) # clearly wrong relative pose
edges.append({"i": 0, "j": N-1, "z": z_bad, "Omega": Omega})
# Initial guess: integrate odometry only
x0 = np.zeros_like(gt)
for k in range(1, N):
z = edges[k-1]["z"]
# compose: x_k = x_{k-1} ∘ z (approx)
th = x0[k-1, 2]
c = np.cos(th); s = np.sin(th)
x0[k, 0] = x0[k-1, 0] + c*z[0] - s*z[1]
x0[k, 1] = x0[k-1, 1] + s*z[0] + c*z[1]
x0[k, 2] = wrap_angle(x0[k-1, 2] + z[2])
return gt, x0, edges
if __name__ == "__main__":
gt, x0, edges = make_synthetic_graph(N=25, seed=1)
print("\n=== Solve without robust kernel (sensitive to outlier) ===")
x_ls = solve_pose_graph_irls(x0, edges, robust="none", n_iters=15, damping=1e-6)
print("\n=== Solve with Huber robust kernel (IRLS) ===")
x_huber = solve_pose_graph_irls(x0, edges, robust="huber", delta=1.5, n_iters=15, damping=1e-6)
# Simple endpoint comparison
print("\nEndpoint (pose N-1):")
print(" ground truth:", gt[-1])
print(" least squares:", x_ls[-1])
print(" huber:", x_huber[-1])
10. C++ Lab — Robust Kernels in Ceres Solver
In Ceres, robust kernels are implemented by attaching a
LossFunction (e.g., HuberLoss) to each residual block. This
example uses a compact 2D relative-pose constraint and includes one bad
loop closure; the Huber loss suppresses its effect.
Chapter12_Lesson4.cpp
// Chapter12_Lesson4.cpp
// Robust pose-graph style constraints using Ceres Solver robust losses (Huber).
// This is a compact example illustrating how robust kernels appear in practice.
// Build (example):
// mkdir build && cd build
// cmake .. && cmake --build .
// Requires: Ceres Solver installed.
#include <ceres/ceres.h>
#include <cmath>
#include <iostream>
#include <vector>
static inline double WrapAngle(double a) {
while (a > M_PI) a -= 2.0 * M_PI;
while (a < -M_PI) a += 2.0 * M_PI;
return a;
}
struct RelativePose2dCost {
RelativePose2dCost(double dx, double dy, double dth, double sxy, double sth)
: dx_(dx), dy_(dy), dth_(dth), sxy_(sxy), sth_(sth) {}
template <typename T>
bool operator()(const T* const pi, const T* const pj, T* residuals) const {
// pi = [xi, yi, thi], pj = [xj, yj, thj]
const T c = ceres::cos(pi[2]);
const T s = ceres::sin(pi[2]);
const T dxw = pj[0] - pi[0];
const T dyw = pj[1] - pi[1];
// relative translation in i-frame
const T xrel = c * dxw + s * dyw;
const T yrel = -s * dxw + c * dyw;
// relative angle
const T threl = ceres::atan2(ceres::sin(pj[2] - pi[2]), ceres::cos(pj[2] - pi[2]));
residuals[0] = (xrel - T(dx_)) / T(sxy_);
residuals[1] = (yrel - T(dy_)) / T(sxy_);
residuals[2] = (threl - T(dth_)) / T(sth_);
return true;
}
double dx_, dy_, dth_;
double sxy_, sth_;
};
int main() {
// Tiny graph: chain + one bad loop closure to show robustness.
const int N = 15;
std::vector<double> poses(3 * N, 0.0);
auto Pose = [&](int i) { return &poses[3 * i]; };
// Initialize along x with small yaw
for (int k = 1; k < N; ++k) {
Pose(k)[0] = Pose(k - 1)[0] + 0.3;
Pose(k)[1] = 0.0;
Pose(k)[2] = 0.02 * k;
}
const double sigma_xy = 0.05;
const double sigma_th = 0.02;
ceres::Problem problem;
// Add all parameter blocks
for (int i = 0; i < N; ++i) {
problem.AddParameterBlock(Pose(i), 3);
}
// Fix first pose (gauge)
problem.SetParameterBlockConstant(Pose(0));
// Odometry edges (near-perfect synthetic)
for (int k = 0; k < N - 1; ++k) {
const double dx = 0.3;
const double dy = 0.0;
const double dth = 0.02;
ceres::CostFunction* cost =
new ceres::AutoDiffCostFunction<RelativePose2dCost, 3, 3, 3>(
new RelativePose2dCost(dx, dy, dth, sigma_xy, sigma_th));
// Robust kernel: Huber loss
ceres::LossFunction* loss = new ceres::HuberLoss(1.5);
problem.AddResidualBlock(cost, loss, Pose(k), Pose(k + 1));
}
// One bad loop closure (outlier)
{
const int i = 0;
const int j = N - 1;
const double dx_bad = 2.0;
const double dy_bad = -1.0;
const double dth_bad = 1.0;
ceres::CostFunction* cost =
new ceres::AutoDiffCostFunction<RelativePose2dCost, 3, 3, 3>(
new RelativePose2dCost(dx_bad, dy_bad, dth_bad, sigma_xy, sigma_th));
// The same robust kernel will downweight this constraint as residual grows.
ceres::LossFunction* loss = new ceres::HuberLoss(1.5);
problem.AddResidualBlock(cost, loss, Pose(i), Pose(j));
}
ceres::Solver::Options options;
options.linear_solver_type = ceres::DENSE_QR;
options.max_num_iterations = 30;
options.minimizer_progress_to_stdout = true;
ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);
std::cout << "\nFinal pose of last node:\n";
std::cout << "x=" << Pose(N - 1)[0] << " y=" << Pose(N - 1)[1]
<< " th=" << Pose(N - 1)[2] << "\n";
return 0;
}
11. Java Lab — IRLS in Pure Java (numeric Jacobians)
This Java implementation mirrors the Python solver: compute residuals, compute Huber weights, assemble normal equations, solve, and iterate. It fixes the first node to remove gauge freedom.
Chapter12_Lesson4.java
// Chapter12_Lesson4.java
// Graph-Based SLAM (2D pose graph) with Huber IRLS (numeric Jacobians) in pure Java.
// Educational: focuses on the robust weighting logic rather than performance.
import java.util.ArrayList;
import java.util.List;
public class Chapter12_Lesson4 {
static class Edge {
int i, j;
double[] z; // [dx, dy, dtheta] = pose_i^{-1} ∘ pose_j in i-frame
double[][] Omega; // 3x3 information matrix
Edge(int i, int j, double[] z, double[][] Omega) {
this.i=i; this.j=j; this.z=z; this.Omega=Omega;
}
}
static double wrap(double a) {
while (a > Math.PI) a -= 2.0 * Math.PI;
while (a < -Math.PI) a += 2.0 * Math.PI;
return a;
}
static double[] between(double[] xi, double[] xj) {
double dx = xj[0] - xi[0];
double dy = xj[1] - xi[1];
double c = Math.cos(xi[2]), s = Math.sin(xi[2]);
double xr = c * dx + s * dy;
double yr = -s * dx + c * dy;
double tr = wrap(xj[2] - xi[2]);
return new double[]{xr, yr, tr};
}
static double huberWeightFromS(double s, double delta) {
double r = Math.sqrt(Math.max(s, 1e-12));
return (r <= delta) ? 1.0 : (delta / r);
}
static class EdgeLinearization {
double[] e; // 3
double[][] Ji; // 3x3
double[][] Jj; // 3x3
EdgeLinearization(double[] e, double[][] Ji, double[][] Jj) {
this.e=e; this.Ji=Ji; this.Jj=Jj;
}
}
static EdgeLinearization numericJacobianEdge(double[] xi, double[] xj, double[] z, double eps) {
double[] e0 = err(xi, xj, z);
double[][] Ji = new double[3][3];
double[][] Jj = new double[3][3];
for (int k=0; k<3; k++) {
double[] d = new double[]{0,0,0};
d[k] = eps;
double[] xi2 = new double[]{xi[0]+d[0], xi[1]+d[1], xi[2]+d[2]};
double[] ei = err(xi2, xj, z);
for (int r=0; r<3; r++) Ji[r][k] = (ei[r]-e0[r]) / eps;
double[] xj2 = new double[]{xj[0]+d[0], xj[1]+d[1], xj[2]+d[2]};
double[] ej = err(xi, xj2, z);
for (int r=0; r<3; r++) Jj[r][k] = (ej[r]-e0[r]) / eps;
}
return new EdgeLinearization(e0, Ji, Jj);
}
static double[] err(double[] xi, double[] xj, double[] z) {
double[] zhat = between(xi, xj);
double[] e = new double[]{zhat[0]-z[0], zhat[1]-z[1], wrap(zhat[2]-z[2])};
return e;
}
static double quadForm(double[] v, double[][] A) {
double[] Av = new double[3];
for (int i=0;i<3;i++){
Av[i]=0;
for(int j=0;j<3;j++) Av[i]+=A[i][j]*v[j];
}
double s=0;
for(int i=0;i<3;i++) s+=v[i]*Av[i];
return s;
}
static double[][] matMulT(double[][] J, double[][] A, double[][] K) {
// return J^T A K ; here all are 3x3
double[][] out = new double[3][3];
for (int r=0;r<3;r++){
for (int c=0;c<3;c++){
double sum=0;
for (int i=0;i<3;i++){
for (int j=0;j<3;j++){
sum += J[i][r] * A[i][j] * K[j][c];
}
}
out[r][c]=sum;
}
}
return out;
}
static double[] matVecMulT(double[][] J, double[][] A, double[] e) {
// return J^T A e
double[] out = new double[3];
for (int r=0;r<3;r++){
double sum=0;
for (int i=0;i<3;i++){
for (int j=0;j<3;j++){
sum += J[i][r] * A[i][j] * e[j];
}
}
out[r]=sum;
}
return out;
}
static void add3(double[][] H, int ra, int ca, double[][] blk) {
for (int r=0;r<3;r++){
for(int c=0;c<3;c++){
H[ra+r][ca+c]+=blk[r][c];
}
}
}
static void add3v(double[] b, int ra, double[] v) {
for(int r=0;r<3;r++) b[ra+r]+=v[r];
}
static double[] solveLinear(double[][] A, double[] b) {
// naive Gaussian elimination (for small systems)
int n = b.length;
double[][] M = new double[n][n];
double[] x = new double[n];
double[] bb = new double[n];
for(int i=0;i<n;i++){
System.arraycopy(A[i], 0, M[i], 0, n);
bb[i]=b[i];
}
// forward
for(int k=0;k<n;k++){
// pivot
int piv=k;
double best=Math.abs(M[k][k]);
for(int i=k+1;i<n;i++){
if(Math.abs(M[i][k])>best){best=Math.abs(M[i][k]);piv=i;}
}
if(piv!=k){
double[] tmp=M[k]; M[k]=M[piv]; M[piv]=tmp;
double t=bb[k]; bb[k]=bb[piv]; bb[piv]=t;
}
double diag=M[k][k];
if(Math.abs(diag)<1e-12) continue;
for(int i=k+1;i<n;i++){
double f=M[i][k]/diag;
for(int j=k;j<n;j++) M[i][j]-=f*M[k][j];
bb[i]-=f*bb[k];
}
}
// back substitution
for(int i=n-1;i>=0;i--){
double sum=bb[i];
for(int j=i+1;j<n;j++) sum-=M[i][j]*x[j];
double diag=M[i][i];
x[i]=(Math.abs(diag)<1e-12)?0:(sum/diag);
}
return x;
}
static double[][] diagOmega(double sxy, double sth) {
double[][] O = new double[3][3];
O[0][0] = 1.0/(sxy*sxy);
O[1][1] = 1.0/(sxy*sxy);
O[2][2] = 1.0/(sth*sth);
return O;
}
static double[][] scaleMat(double a, double[][] M) {
double[][] out = new double[3][3];
for(int i=0;i<3;i++) for(int j=0;j<3;j++) out[i][j]=a*M[i][j];
return out;
}
public static void main(String[] args) {
int N = 20;
double[][] x = new double[N][3];
// initial chain
for (int k=1;k<N;k++){
x[k][0] = x[k-1][0] + 0.3;
x[k][1] = 0.0;
x[k][2] = wrap(x[k-1][2] + 0.05);
}
double sigmaXY=0.05, sigmaTH=0.02;
double[][] Omega = diagOmega(sigmaXY, sigmaTH);
List<Edge> edges = new ArrayList<>();
for(int k=0;k<N-1;k++){
edges.add(new Edge(k, k+1, new double[]{0.3,0.0,0.05}, Omega));
}
// bad loop closure (outlier)
edges.add(new Edge(0, N-1, new double[]{2.0,-1.0,1.0}, Omega));
// Optimize with IRLS-Huber
int iters=15;
double delta=1.5;
double eps=1e-6;
double damping=1e-6;
// Fix node 0 (remove gauge): variables are nodes 1..N-1
int varN = N-1;
int dim = 3*varN;
for(int it=0; it<iters; it++){
double[][] H = new double[dim][dim];
double[] b = new double[dim];
double sumS=0, sumRho=0;
for(Edge ed : edges){
int i=ed.i, j=ed.j;
EdgeLinearization lin = numericJacobianEdge(x[i], x[j], ed.z, eps);
double s = quadForm(lin.e, ed.Omega);
sumS += s;
double w = huberWeightFromS(s, delta);
double r = Math.sqrt(Math.max(s, 1e-12));
double rho = (r <= delta) ? s : (2.0*delta*r - delta*delta);
sumRho += rho;
double[][] W = scaleMat(w, ed.Omega);
// blocks if variable (skip node 0)
Integer ci = (i==0)? null : 3*(i-1);
Integer cj = (j==0)? null : 3*(j-1);
// Hii, Hij, gj
if(ci != null){
double[][] Hii = matMulT(lin.Ji, W, lin.Ji);
double[] gi = matVecMulT(lin.Ji, W, lin.e);
add3(H, ci, ci, Hii);
add3v(b, ci, gi);
}
if(cj != null){
double[][] Hjj = matMulT(lin.Jj, W, lin.Jj);
double[] gj = matVecMulT(lin.Jj, W, lin.e);
add3(H, cj, cj, Hjj);
add3v(b, cj, gj);
}
if(ci != null && cj != null){
double[][] Hij = matMulT(lin.Ji, W, lin.Jj);
double[][] Hji = matMulT(lin.Jj, W, lin.Ji);
add3(H, ci, cj, Hij);
add3(H, cj, ci, Hji);
}
}
// damping
for(int d=0; d<dim; d++) H[d][d] += damping;
// solve H dx = -b
double[] rhs = new double[dim];
for(int k=0;k<dim;k++) rhs[k] = -b[k];
double[] dx = solveLinear(H, rhs);
double norm=0;
for(double v: dx) norm += v*v;
norm = Math.sqrt(norm);
// apply update
for(int node=1; node<N; node++){
int c = 3*(node-1);
x[node][0] += dx[c+0];
x[node][1] += dx[c+1];
x[node][2] = wrap(x[node][2] + dx[c+2]);
}
System.out.printf("iter %02d sumS=%.3f sumRho=%.3f |dx|=%.3e%n", it, sumS, sumRho, norm);
if(norm < 1e-8) break;
}
System.out.println("Final pose of last node: x=" + x[N-1][0] + " y=" + x[N-1][1] + " th=" + x[N-1][2]);
}
}
12. MATLAB/Simulink Lab — IRLS Pose Graph Loop
The MATLAB script below implements IRLS-Huber. For Simulink, place the IRLS step in a MATLAB Function block that outputs \( \Delta\mathbf{x} \), then integrate pose states with Unit Delay blocks inside a For-Iterator subsystem (see comments in code).
Chapter12_Lesson4.m
% Chapter12_Lesson4.m
% Graph-Based SLAM (2D pose graph) with robust kernels via IRLS (Huber).
% Educational MATLAB implementation using numeric Jacobians and Gauss-Newton.
function Chapter12_Lesson4()
rng(1);
N = 20;
x = zeros(N,3);
for k=2:N
x(k,1) = x(k-1,1) + 0.3;
x(k,2) = 0.0;
x(k,3) = wrap(x(k-1,3) + 0.05);
end
sigmaXY = 0.05; sigmaTH = 0.02;
Omega = diag([1/sigmaXY^2, 1/sigmaXY^2, 1/sigmaTH^2]);
edges = {};
for k=1:N-1
e.i = k; e.j = k+1;
e.z = [0.3; 0.0; 0.05];
e.Omega = Omega;
edges{end+1} = e;
end
% bad loop closure (outlier)
e.i = 1; e.j = N; e.z = [2.0; -1.0; 1.0]; e.Omega = Omega;
edges{end+1} = e;
delta = 1.5;
iters = 15;
damping = 1e-6;
% Fix node 1 => variables nodes 2..N
dim = 3*(N-1);
for it=1:iters
H = zeros(dim,dim);
b = zeros(dim,1);
sumS = 0; sumRho = 0;
for idx=1:numel(edges)
ed = edges{idx};
i = ed.i; j = ed.j;
[eij, Ji, Jj] = numericJacobianEdge(x(i,:)' , x(j,:)' , ed.z, 1e-6);
s = eij' * ed.Omega * eij;
sumS = sumS + s;
w = huberWeightFromS(s, delta);
r = sqrt(max(s, 1e-12));
if r <= delta
rho = s;
else
rho = 2*delta*r - delta^2;
end
sumRho = sumRho + rho;
W = w * ed.Omega;
ci = nodeToCol(i, N);
cj = nodeToCol(j, N);
if ci > 0
H(ci:ci+2, ci:ci+2) = H(ci:ci+2, ci:ci+2) + Ji' * W * Ji;
b(ci:ci+2) = b(ci:ci+2) + Ji' * W * eij;
end
if cj > 0
H(cj:cj+2, cj:cj+2) = H(cj:cj+2, cj:cj+2) + Jj' * W * Jj;
b(cj:cj+2) = b(cj:cj+2) + Jj' * W * eij;
end
if (ci > 0) && (cj > 0)
H(ci:ci+2, cj:cj+2) = H(ci:ci+2, cj:cj+2) + Ji' * W * Jj;
H(cj:cj+2, ci:ci+2) = H(cj:cj+2, ci:ci+2) + Jj' * W * Ji;
end
end
H = 0.5*(H+H') + damping*eye(dim);
dx = -H \ b;
% apply
for node=2:N
c = 3*(node-2) + 1;
x(node,1) = x(node,1) + dx(c+0);
x(node,2) = x(node,2) + dx(c+1);
x(node,3) = wrap(x(node,3) + dx(c+2));
end
fprintf('iter %02d sumS=%.3f sumRho=%.3f |dx|=%.3e\n', it, sumS, sumRho, norm(dx));
if norm(dx) < 1e-8
break;
end
end
disp('Final pose of last node:');
disp(x(end,:));
% Simulink note:
% - You can implement the IRLS loop with a MATLAB Function block that outputs dx,
% then integrate pose states with Unit Delay blocks, using a For-Iterator subsystem.
end
function c = nodeToCol(node, N)
% node 1 fixed => return 0. Others map to 1-based column index.
if node == 1
c = 0;
else
c = 3*(node-2) + 1;
end
end
function w = huberWeightFromS(s, delta)
r = sqrt(max(s, 1e-12));
if r <= delta
w = 1.0;
else
w = delta / r;
end
end
function a = wrap(a)
a = mod(a + pi, 2*pi) - pi;
end
function z = between(xi, xj)
dx = xj(1) - xi(1);
dy = xj(2) - xi(2);
c = cos(xi(3)); s = sin(xi(3));
xr = c*dx + s*dy;
yr = -s*dx + c*dy;
tr = wrap(xj(3) - xi(3));
z = [xr; yr; tr];
end
function e = err(xi, xj, z)
zhat = between(xi, xj);
e = zhat - z;
e(3) = wrap(e(3));
end
function [e0, Ji, Jj] = numericJacobianEdge(xi, xj, z, eps)
e0 = err(xi, xj, z);
Ji = zeros(3,3);
Jj = zeros(3,3);
for k=1:3
d = zeros(3,1); d(k)=eps;
ei = err(xi+d, xj, z);
Ji(:,k) = (ei - e0)/eps;
ej = err(xi, xj+d, z);
Jj(:,k) = (ej - e0)/eps;
end
end
13. Wolfram Mathematica — Symbolic Weight Derivation + IRLS Demo
This notebook expression derives the Huber weight \( w(s)=\rho'(s) \) and demonstrates IRLS on a 1D robust location problem.
Chapter12_Lesson4.nb
(* Chapter12_Lesson4.nb
Wolfram Language notebook expression (plain-text).
Focus: deriving robust weights and a small IRLS demo for 1D location with outliers. *)
Notebook[{
Cell["Chapter 12 - Lesson 4: Handling Outliers (Robust Kernels)", "Title"],
Cell["IRLS weight derivation for Huber kernel (rho applied to s = r^2).", "Section"],
Cell[BoxData[
RowBox[{
RowBox[{"rho", "[", "s_", "]"}], ":=",
RowBox[{"Piecewise", "[",
RowBox[{
RowBox[{"{",
RowBox[{
RowBox[{"{", RowBox[{"s", ",", RowBox[{"Sqrt", "[", "s", "]"}], "<=", "c"}], "}"}],
",",
RowBox[{"{", RowBox[{
RowBox[{
RowBox[{"2", " ", "c", " ", RowBox[{"Sqrt", "[", "s", "]"}]}], "-", RowBox[{"c", "^", "2"}]
}],
",",
RowBox[{"Sqrt", "[", "s", "]"}], ">", "c"
}], "}"}]
}], "}"}],
",", "s"
}], "]"}]}]
], "Input"],
Cell["Derivative and IRLS weight w = rho'(s).", "Text"],
Cell[BoxData[
RowBox[{
RowBox[{"w", "[", "s_", "]"}], ":=",
RowBox[{"D", "[",
RowBox[{
RowBox[{"rho", "[", "s", "]"}], ",", "s"
}], "]"}]}]
], "Input"],
Cell["Check piecewise form of w(s):", "Text"],
Cell[BoxData[RowBox[{"FullSimplify", "[", RowBox[{"w", "[", "s", "]"}], "]"}]], "Input"],
Cell["IRLS for 1D location: minimize Sum rho((x - yi)^2) over x.", "Section"],
Cell[BoxData[
RowBox[{
RowBox[{"ys", "=", RowBox[{"Join", "[",
RowBox[{
RowBox[{"RandomVariate", "[", RowBox[{"NormalDistribution", "[", RowBox[{"0", ",", "1"}], "]"}], ",", "40"}], "]"}],
",",
RowBox[{"{", RowBox[{"8", ",", "9", ",", "10"}], "}"}]
}], "]"}]}], ";"}]
], "Input"],
Cell[BoxData[RowBox[{"c", "=", "1.5"}]], "Input"],
Cell[BoxData[
RowBox[{
RowBox[{"irls", "[", RowBox[{"x0_", ",", "iters_"}], "]"}], ":=",
RowBox[{"Module", "[",
RowBox[{
RowBox[{"{", RowBox[{"x", "=", "x0", ",", "r", ",", "s", ",", "ws"}], "}"}],
",",
RowBox[{"Do", "[",
RowBox[{
RowBox[{
RowBox[{"r", "=", RowBox[{"x", "-", "ys"}]}], ";",
RowBox[{"s", "=", RowBox[{"r", "^", "2"}]}], ";",
RowBox[{"ws", "=", RowBox[{"w", " /@ ", "s"}]}], ";",
RowBox[{"x", "=", RowBox[{"Total", "[", RowBox[{"ws", " ", "ys"}], "]"}], "/", RowBox[{"Total", "[", "ws", "]"}]}], ";"
}],
",",
RowBox[{"{", RowBox[{"iters"}], "}"}]
}], "]"}], ";",
"x"
}], "]"}]}]
], "Input"],
Cell[BoxData[RowBox[{"x_hat", "=", RowBox[{"irls", "[", RowBox[{"0", ",", "25"}], "]"}]}]], "Input"],
Cell[BoxData[RowBox[{"{", RowBox[{"Mean", "[", "ys", "]"}], ",", "x_hat"}], "}"}]], "Input"],
Cell["The robust estimate x_hat is less influenced by the extreme outliers than Mean[ys].", "Text"]
}]
14. Problems and Solutions
Problem 1 (IRLS surrogate): Starting from \( \min_{\mathbf{x}}\sum_i \rho(s_i(\mathbf{x})) \) with \( s_i=\mathbf{r}_i^{\top}\Omega_i\mathbf{r}_i \), justify the IRLS surrogate \( \min_{\Delta\mathbf{x}}\sum_i w_i\|\mathbf{r}_i+\mathbf{J}_i\Delta\mathbf{x}\|_{\Omega_i}^2 \) with \( w_i=\rho'(s_i^{(k)}) \).
Solution: Let \( s_i(\Delta\mathbf{x})=(\mathbf{r}_i+\mathbf{J}_i\Delta\mathbf{x})^{\top}\Omega_i(\mathbf{r}_i+\mathbf{J}_i\Delta\mathbf{x}) \). Taylor expand \( \rho \) around \( s_i^{(k)} \):
\[ \rho(s_i(\Delta\mathbf{x})) \approx \rho(s_i^{(k)}) + \rho'(s_i^{(k)})(s_i(\Delta\mathbf{x})-s_i^{(k)}). \]
Dropping constants yields \( \min_{\Delta\mathbf{x}} \sum_i \rho'(s_i^{(k)})\,s_i(\Delta\mathbf{x}) \), which is weighted least squares with \( w_i=\rho'(s_i^{(k)}) \).
Problem 2 (Huber derivative): For \( \rho(s)=s \) if \( \sqrt{s}\le\delta \) and \( \rho(s)=2\delta\sqrt{s}-\delta^2 \) if \( \sqrt{s}\gt\delta \), compute \( w(s)=\rho'(s) \).
Solution: If \( \sqrt{s}\le\delta \), \( \rho'(s)=1 \). If \( \sqrt{s}\gt\delta \), then \( \rho(s)=2\delta s^{1/2}-\delta^2 \Rightarrow \rho'(s)=\delta/\sqrt{s} \).
\[ w(s)= \begin{cases} 1, & \sqrt{s}\le \delta \\ \delta/\sqrt{s}, & \sqrt{s} \gt \delta. \end{cases} \]
Problem 3 (MAP form): Assume independent likelihoods \( p(\mathbf{z}_i\mid\mathbf{x})\propto \exp(-\tfrac{1}{2}\rho(s_i(\mathbf{x}))) \). Derive the MAP objective.
Solution: \( p(\mathcal{Z}\mid\mathbf{x})\propto \prod_i \exp(-\tfrac{1}{2}\rho(s_i(\mathbf{x}))) \). Negative log (dropping constants) gives \( \min_{\mathbf{x}} \sum_i \rho(s_i(\mathbf{x})) \).
Problem 4 (Chi-square tuning): Residual dimension is \( d=3 \) and inliers are Gaussian. Choose \( \delta \) so that \( \mathbb{P}(s\le\delta^2)=0.95 \).
Solution: Under inlier Gaussianity, \( s\sim\chi^2_3 \) approximately. Choose \( \delta=\sqrt{\chi^2_{3,0.95}} \).
Problem 5 (Bounded influence vs outlier domination): Explain why least squares can be dominated by a single outlier, but Huber cannot.
Solution: Least squares has unbounded influence \( \psi(r)=2r \), so as \( |r| \) grows, its effect grows without limit. Huber has bounded influence for \( |r| \gt \delta \), namely \( \psi(r)=2\delta\operatorname{sign}(r) \), so a single outlier cannot create arbitrarily large pull.
15. Summary
Robust kernels replace \( \sum s_{ij} \) with \( \sum \rho(s_{ij}) \), limiting the influence of false constraints. They admit a clean MAP interpretation via heavy-tailed likelihoods. IRLS enables practical robust SLAM by repeatedly solving weighted least squares using the same sparse Gauss–Newton/LM machinery, simply scaling each edge’s information by \( w_i=\rho'(s_i) \).
16. References
- Huber, P.J. (1964). Robust estimation of a location parameter. Annals of Mathematical Statistics, 35(1), 73–101.
- Hampel, F.R. (1974). The influence curve and its role in robust estimation. Journal of the American Statistical Association, 69(346), 383–393.
- Black, M.J., & Rangarajan, A. (1996). On the unification of line processes, outlier rejection, and robust statistics with applications in early vision. International Journal of Computer Vision, 19(1), 57–91.
- Triggs, B., McLauchlan, P.F., Hartley, R.I., & Fitzgibbon, A.W. (2000). Bundle adjustment — a modern synthesis. Vision Algorithms: Theory and Practice (LNCS 1883), 298–372.
- Kümmerle, R., Grisetti, G., Strasdat, H., Konolige, K., & Burgard, W. (2011). g2o: A general framework for graph optimization. IEEE International Conference on Robotics and Automation (ICRA), 3607–3613.
- Sünderhauf, N., & Protzel, P. (2012). Switchable constraints for robust pose graph SLAM. IEEE/RSJ International Conference on Intelligent Robots and Systems (IROS), 1879–1884.
- Agarwal, P., Tipaldi, G.D., Spinello, L., Stachniss, C., & Burgard, W. (2013). Robust map optimization using dynamic covariance scaling. IEEE International Conference on Robotics and Automation (ICRA), 62–69.
- Dellaert, F. (2012). Factor graphs and GTSAM: A hands-on introduction. Technical report, Georgia Institute of Technology.