good luck twin.
- Move to using SE(2) for pose and to add the scan matching correction (no compose_se2)
- The entirety of graph SLAM (and loop closure!).
- Speed up critical components (further) (looking at you c++ bindings)
Moving from a simple "naive addition" loop to Graph SLAM is a significant architectural shift. You are moving from a Filter mindset (forgetting the past) to an Optimization mindset (revising the past when new info arrives).
Here is your roadmap for integrating GTSAM and transitioning to iSAM.
In Graph SLAM, your "pose" is no longer a single variable; it is a series of nodes () connected by edges (constraints).
-
[ ] Define Your Noise Models: GTSAM requires "Uncertainty" for every edge.
-
Odom Noise: Usually constant (e.g.,
gtsam.noiseModel.Diagonal.Sigmas([0.05, 0.05, 0.01])). -
ICP Noise: Should ideally scale with the ICP fitness score.
-
[ ] Initialize the Factor Graph: * Create a
gtsam.NonlinearFactorGraph(). -
Add a PriorFactor at to "anchor" the map to the origin .
- [ ] Create the Nodes: Instead of
naive_add *= delta, you will now increment a key:X(i). - [ ] Add Odometry Factors: Between and , add a
BetweenFactor. Use yourodom_deltaas the measurement. - [ ] Add Scan Match Factors: Add a second
BetweenFactor(or fuse them) between and using yoursm_delta. - [ ] Store Initial Estimates: You must provide GTSAM with a "guess" for where the robot is. Your current
naive_addlogic serves as the perfect initial estimate for each new node.
- [ ] Batch Optimization: Every frames (or at the end), run
gtsam.LevenbergMarquardtOptimizer(graph, initial_estimates).optimize(). - [ ] Map Correction: When the optimizer finishes, it returns the corrected values for all previous poses. You must re-render your Occupancy Grid or Rerun trajectory using these corrected values.
Batch optimization (Levenberg-Marquardt) gets slower as the map grows. iSAM2 (Incremental Smoothing and Mapping) uses a Bayes Tree to only update the parts of the graph that actually changed.
Switch from LevenbergMarquardtOptimizer to gtsam.ISAM2.
parameters = gtsam.ISAM2Params()
parameters.setRelinearizeThreshold(0.1)
isam = gtsam.ISAM2(parameters)Unlike batch optimization, you don't keep the whole graph in memory. You push "new" factors and "new" estimates to iSAM2 every frame:
- Add new factors to a temporary graph.
- Add new initial estimate to a temporary
Valuesobject. - Call
isam.update(new_factors, new_estimates). - Retrieve the current corrected pose:
isam.calculateEstimate(X(i)).
The primary reason to use iSAM2 is loop closure.
- The Logic: If your current pose is near an old pose , run an ICP match between the current scan and the old scan.
- The Factor: If the match is good, add a
BetweenFactorconnecting and . - The Result: iSAM2 will instantly "snap" the entire trajectory to eliminate the drift accumulated over time.
- [ ] Global vs. Local Mapping: In your
update_scanfor the occupancy grid, use the corrected pose fromisam.calculateEstimate. - [ ] Consistency Check: Monitor the "Chi-squared" error of the graph. If it spikes, your ICP probably gave a "false positive" match (a bad loop closure).
- [ ] Fixed-Lag Smoothing: If memory becomes an issue, tell iSAM2 to "forget" factors older than seconds to keep the graph size constant.
To implement Graph SLAM with GTSAM and eventually migrate to iSAM2, you need to move away from treating your robot as a "moving point" and start treating it as a collection of constraints.
Here is the documentation of the specific GTSAM tools and concepts you will need to integrate into your current loop.
In a graph, every pose needs a unique ID. Using symbols allows you to distinguish between different types of variables (e.g., Robot Poses vs. Landmarks).
- Usage: Use
gtsam.symbol('x', i)to create keys for your poses (). This prevents index collisions if you later add landmarks ('l').
This is the "container" for your SLAM world. It doesn't store the positions; it only stores the relationships (factors) between them.
- Role: You will
.add()factors here every time you get an odometry update or a scan match.
This is a dictionary that stores the actual coordinates (the "Initial Estimates").
- Role: Every time you create a new Pose node in the graph, you must give it a "guess" of where it is. You will use your current
naive_addresult as the value for the latest key.
A graph with only relative movements can float away in space. You need at least one "anchor."
- Documentation: This fixes a specific variable (usually ) to a specific coordinate (usually ) with a very high confidence (low noise).
This is the workhorse for your project. It represents a relative transformation between two poses.
- For Odometry: Connects to using your
odom_delta. - For Scan Matching: Connects to using your
sm_delta. - For Loop Closure: Connects to when you revisit a location.
GTSAM is probabilistic. You cannot add a measurement without telling the graph how much you trust it.
- Usage: Define a 3-element vector: .
- Logic: If your steering is noisy but your RPM is precise, you would set a higher sigma for and a lower one for .
- Usage: Wraps a standard noise model in a "Kernel" (like Cauchy or Huber).
- Role: Since you are already using a Cauchy loss in your ICP, wrapping your Scan Match factors in a Robust kernel prevents one "bad match" (outlier) from pulling your entire graph apart.
The "Batch" solver. It takes the entire graph and all values, looks at the total error, and shifts every pose simultaneously to minimize it.
- Best for: Post-processing a run or "Global" re-alignment.
The "Incremental" solver. It uses a data structure called a Bayes Tree to perform partial updates.
- Key Tool:
ISAM2Params: You’ll need to tunerelinearizeThreshold. This determines how much the robot has to move before iSAM2 recalculates the nonlinear parts of the graph. - Key Tool:
ISAM2Result: Provides metadata on the update, such as how many variables were recalculated.
| Feature | Your Current Loop | GTSAM Graph SLAM | iSAM2 (Incremental) |
|---|---|---|---|
| Pose Storage | One SE2 variable |
gtsam.Values (All history) |
gtsam.Values (Recent/Relevant) |
| Update Step | Matrix multiplication | Add Factor Batch Optimize | Add Factor isam.update() |
| Drift | Accumulates forever | Distributed across graph | Corrected via Loop Closure |
| Complexity | (Grows with time) | (Near constant) |
Would you like me to explain how to calculate the Covariance matrix from your ICP results so you can feed a mathematically "honest" NoiseModel into GTSAM?