<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>ROS | Saikiran Juttu | Robotics Portfolio</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/tags/ros/</link><atom:link href="https://juttu-s.github.io/saikiran_juttu.github.io/tags/ros/index.xml" rel="self" type="application/rss+xml"/><description>ROS</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Fri, 26 Apr 2024 00:00:00 +0000</lastBuildDate><image><url>https://juttu-s.github.io/saikiran_juttu.github.io/media/icon_hu7729264130191091259.png</url><title>ROS</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/tags/ros/</link></image><item><title>Kimera VIO on EuRoC and Custom Datasets</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/project/kimera-vio/</link><pubDate>Fri, 26 Apr 2024 00:00:00 +0000</pubDate><guid>https://juttu-s.github.io/saikiran_juttu.github.io/project/kimera-vio/</guid><description>&lt;p>An evaluation and deployment project rather than an algorithms one. &lt;a href="https://github.com/MIT-SPARK/Kimera-VIO" target="_blank" rel="noopener">Kimera-VIO&lt;/a> from MIT-SPARK was used as shipped; the work here was getting it running reproducibly, reproducing its published benchmark on EuRoC, and then pointing it at a sequence it had never seen — recorded on an Intel RealSense D455 in an ordinary office — to find out what breaks when you leave the benchmark behind.&lt;/p>
&lt;hr>
&lt;h2 id="what-kimera-vio-is-doing">What Kimera-VIO is doing&lt;/h2>
&lt;p>Worth stating, because the interesting failure later only makes sense against it.&lt;/p>
&lt;p>&lt;strong>Front end.&lt;/strong> Shi-Tomasi corners on the left stereo image, tracked temporally with Lucas–Kanade optical flow, matched left-to-right and triangulated for depth, with outlier rejection on the resulting correspondences. IMU measurements between keyframes are preintegrated into a single relative-motion constraint and used for gravity alignment.&lt;/p>
&lt;p>&lt;strong>Back end.&lt;/strong> A factor graph over position, orientation and velocity, with IMU factors and visual reprojection factors as edges, optimised incrementally by &lt;strong>iSAM2&lt;/strong> in GTSAM. Robust losses (Huber, Cauchy) absorb the outliers that survive the front end. Output is a pose trajectory, velocity estimates and a sparse landmark map.&lt;/p>
&lt;p>&lt;strong>Loop closure.&lt;/strong> Kimera-RPGO detects loops and runs robust pose-graph optimisation over them to produce a globally consistent trajectory.&lt;/p>
&lt;h2 id="reproducibility">Reproducibility&lt;/h2>
&lt;p>The deliverable that mattered most was a &lt;strong>saved Docker image&lt;/strong> with Kimera-VIO, ROS and every dependency preinstalled — distributed as a tarball so the whole pipeline comes up in three commands:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker load -i kimera-vio.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">docker run -it kimera-vio
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python3 visual.py data_pgo.csv &lt;span class="c1"># inside the container&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Anyone who has built a VIO stack from source knows why this is worth doing. Kimera-VIO wants a specific GTSAM, which wants a specific Boost and Eigen, and the failure mode of getting it wrong is a pipeline that compiles and then silently estimates garbage. Pinning the whole environment means a result someone else can actually reproduce, and it makes &amp;ldquo;does this parameter change help?&amp;rdquo; a question you can answer in an afternoon instead of a week.&lt;/p>
&lt;p>Stereo image pairs and IMU streams were synchronised and fed in through ROS.&lt;/p>
&lt;hr>
&lt;h2 id="euroc-benchmark">EuRoC benchmark&lt;/h2>
&lt;p>Run on the &lt;strong>EuRoC MAV&lt;/strong> Vicon Room 1 sequences — &lt;code>V1_01&lt;/code>, &lt;code>V1_02&lt;/code> and &lt;code>V1_03&lt;/code>, spanning easy, medium and difficult, with motion-capture ground truth. That&amp;rsquo;s three of the dataset&amp;rsquo;s eleven sequences; the Machine Hall and Vicon Room 2 sets weren&amp;rsquo;t covered.&lt;/p>
&lt;p>ATE RMSE in metres, reproducing the published comparison across VIO pipelines:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Sequence&lt;/th>
&lt;th>OKVIS&lt;/th>
&lt;th>MSCKF&lt;/th>
&lt;th>ROVIO&lt;/th>
&lt;th>VINS-Mono&lt;/th>
&lt;th>Kimera-VIO&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>V1_01&lt;/td>
&lt;td>0.09&lt;/td>
&lt;td>0.34&lt;/td>
&lt;td>0.10&lt;/td>
&lt;td>0.08&lt;/td>
&lt;td>&lt;strong>0.05&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>V1_02&lt;/td>
&lt;td>0.20&lt;/td>
&lt;td>0.20&lt;/td>
&lt;td>0.10&lt;/td>
&lt;td>0.11&lt;/td>
&lt;td>&lt;strong>0.08&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>V1_03&lt;/td>
&lt;td>0.24&lt;/td>
&lt;td>0.67&lt;/td>
&lt;td>0.14&lt;/td>
&lt;td>0.18&lt;/td>
&lt;td>&lt;strong>0.07&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The pattern worth noting is the third row. Every other pipeline degrades on &lt;code>V1_03&lt;/code> — the difficult sequence, with faster and more aggressive motion — while Kimera-VIO holds at 0.07 m. Aggressive motion is &lt;em>good&lt;/em> for a visual-inertial system: it excites the IMU, which is what makes scale and gravity direction observable in the first place. Getting parameters into the range where that benchmark reproduces meant tuning IMU noise models, feature thresholds and optimiser tolerances. Throughput came out around &lt;strong>25 FPS&lt;/strong>.&lt;/p>
&lt;h2 id="custom-dataset">Custom dataset&lt;/h2>
&lt;p>Recorded with a &lt;strong>RealSense D455&lt;/strong> in a room of desks, monitors and the usual office clutter, walking an approximately oval path back to the starting point.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="The post-optimisation trajectory recovering the oval path walked during recording" srcset="
/saikiran_juttu.github.io/project/kimera-vio/trajectory_hu18271531692901669461.webp 400w,
/saikiran_juttu.github.io/project/kimera-vio/trajectory_hu14690658229423064687.webp 760w,
/saikiran_juttu.github.io/project/kimera-vio/trajectory_hu16662803654171847119.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/kimera-vio/trajectory_hu18271531692901669461.webp"
width="760"
height="576"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>Trajectory after pose-graph optimisation. The plot renders the path in the x–y plane, so vertical motion isn&amp;rsquo;t shown.&lt;/em>&lt;/p>
&lt;p>There is &lt;strong>no ground truth&lt;/strong> for this sequence — no motion capture in an office — so everything here is qualitative. No ATE, no drift percentage, and any number attached to it would be invented. What it does show is loop closure working: the path returns to where it started rather than spiralling away.&lt;/p>
&lt;h3 id="starting-at-rest">Starting at rest&lt;/h3>
&lt;p>The most useful thing this sequence taught me was a failure at the very beginning of the recording, and it isn&amp;rsquo;t a bug — it&amp;rsquo;s a property of the problem.&lt;/p>
&lt;p>Visual-inertial odometry recovers metric scale and gravity direction from the accelerometer. But an accelerometer at rest measures only gravity, and there is no way to separate &amp;ldquo;which way is down&amp;rdquo; from &amp;ldquo;how fast am I accelerating&amp;rdquo; without motion to disambiguate them. Start recording with the camera stationary and the initialiser has no excitation to work with; it converges on a poor estimate, and that error is baked into every pose that follows.&lt;/p>
&lt;p>That&amp;rsquo;s exactly what happened — visible drift accumulated from the start of the trajectory, and it persisted until loop closure corrected it. It also explains the EuRoC pattern above: the &amp;ldquo;difficult&amp;rdquo; sequence is easier for VIO in this one specific respect. The practical lesson is that a data-collection protocol matters as much as a config file: move the camera through a few seconds of varied motion &lt;strong>before&lt;/strong> anything you care about, and the whole run improves.&lt;/p>
&lt;p>Elsewhere in the sequence, low-texture regions produced feature sparsity and minor drift, which pose-graph optimisation largely absorbed.&lt;/p>
&lt;h2 id="limits-and-next-steps">Limits and next steps&lt;/h2>
&lt;p>Kimera-VIO&amp;rsquo;s weak points on this data were the ones its authors document: drift in low-texture regions, and sensitivity to outlier visual measurements. The obvious extensions, in order of how much they&amp;rsquo;d change the picture:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Metric-semantic mapping.&lt;/strong> This used the VIO half of the Kimera suite only. Kimera-Semantics builds a semantically annotated mesh from the same pipeline, which is where the interesting downstream work is.&lt;/li>
&lt;li>&lt;strong>Outdoor and larger scale&lt;/strong>, to see where the factor graph stops being tractable.&lt;/li>
&lt;li>&lt;strong>LiDAR as an additional modality&lt;/strong>, for the feature-poor cases where vision alone degrades.&lt;/li>
&lt;/ul>
&lt;h2 id="stack">Stack&lt;/h2>
&lt;p>Kimera-VIO (MIT-SPARK), GTSAM, ROS, Docker, Python, matplotlib. Datasets: EuRoC MAV V1_01–V1_03; self-collected RealSense D455 stereo + IMU.&lt;/p></description></item><item><title>Semantic Geometric SLAM (SG-SLAM) in Dynamic Scenes</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/project/sg-slam/</link><pubDate>Fri, 19 Apr 2024 00:00:00 +0000</pubDate><guid>https://juttu-s.github.io/saikiran_juttu.github.io/project/sg-slam/</guid><description>&lt;p>Feature-based visual SLAM assumes the world holds still. It doesn&amp;rsquo;t. When someone walks through the frame, their features get matched across keyframes like any other, and the optimiser dutifully fits a camera trajectory to a person who was never part of the scene geometry. On the TUM &lt;code>fr3/walking_*&lt;/code> sequences this is not a marginal degradation — ORB-SLAM2&amp;rsquo;s absolute trajectory error goes to roughly half a metre.&lt;/p>
&lt;p>This project reimplements &lt;strong>SG-SLAM&lt;/strong> (&lt;a href="https://ieeexplore.ieee.org/document/10018238" target="_blank" rel="noopener">Cheng, Sun, Zhang &amp;amp; Zhang, &lt;em>IEEE TIM&lt;/em> vol. 72, 2023&lt;/a>), which addresses this by adding two parallel threads to ORB-SLAM2 — one for object detection, one for semantic mapping — and a feature-rejection stage in the tracking thread that consults both geometry and semantics before deciding what to keep.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="SG-SLAM running on a live RGB-D stream: ORB features tracked on the static scene while a person walks through the frame, the keyframe graph, and the semantically labelled point cloud in RViz" srcset="
/saikiran_juttu.github.io/project/sg-slam/system_hu9625658651780549619.webp 400w,
/saikiran_juttu.github.io/project/sg-slam/system_hu10093939004686469362.webp 760w,
/saikiran_juttu.github.io/project/sg-slam/system_hu15078296984376168334.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/sg-slam/system_hu9625658651780549619.webp"
width="760"
height="422"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>The three threads running together. Left: the live frame — note the green ORB features sit on desks, walls and shelving, not on the person. Bottom: the reconstructed point cloud with detected objects labelled and localised.&lt;/em>&lt;/p>
&lt;hr>
&lt;h2 id="why-not-just-mask-out-the-people">Why not just mask out the people&lt;/h2>
&lt;p>The obvious approach is to run a detector, draw boxes around every &lt;em>a priori&lt;/em> dynamic class, and discard whatever falls inside. That fails in both directions, and SG-SLAM&amp;rsquo;s design is a direct response to each failure:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Things move that aren&amp;rsquo;t in a box.&lt;/strong> A detector trained on 20 classes will miss a swinging door, a rolling cart, a shadow. Masking gives you no protection outside the boxes it drew.&lt;/li>
&lt;li>&lt;strong>Things in boxes aren&amp;rsquo;t always moving.&lt;/strong> A parked chair, an empty sofa, a person sitting perfectly still — all get classified dynamic, all get thrown away. In a cluttered indoor scene that can mean discarding the best-textured features you have.&lt;/li>
&lt;/ul>
&lt;h2 id="the-rejection-criterion">The rejection criterion&lt;/h2>
&lt;p>The mechanism that avoids both is worth reading closely, because it is subtler than a mask. Reproduced from the paper&amp;rsquo;s Algorithm 1:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Input: Previous frame F1, current frame F2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Feature points P1 (previous), P2 (current)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Standard empirical threshold e_std
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Output: Set S of static feature points in the current frame
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 1: P1 = CalcOpticalFlowPyrLK(F2, F1, P2)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 2: Remove matched pairs at image edges or with large appearance variation
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 3: F = FindFundamentalMat(P2, P1, 7-point method with RANSAC)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 4: for each matched pair (p1, p2) in (P1, P2) do
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 5: if DynamicObjectsExist and IsInDynamicRegion(p2) then
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 6: if CalcEpiLineDistance(p2, p1, F) * GetDynamicWeightValue(p2) &amp;lt; e_std then
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 7: append p2 to S
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 8: end if
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> 9: else
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">10: if CalcEpiLineDistance(p2, p1, F) &amp;lt; e_std then
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">11: append p2 to S
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">12: end if
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">13: end if
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">14: end for
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>There is &lt;strong>one&lt;/strong> threshold, &lt;code>e_std&lt;/code>, and it is compared against a point-to-epipolar-line distance in both branches. The difference is that inside a detected region (line 6) that distance is first &lt;strong>multiplied&lt;/strong> by the object class&amp;rsquo;s dynamic weight.&lt;/p>
&lt;p>So semantics act as a per-class multiplier on the geometric residual, not as a veto. A high-weight class like a person has its residual inflated, so it gets rejected on much weaker geometric evidence than a low-weight class like a chair. But a genuinely stationary object still passes — its epipolar distance is near zero, and scaling near-zero by three is still near-zero. Meanwhile line 10 runs everywhere the detector saw nothing, so unmodelled motion is still caught geometrically.&lt;/p>
&lt;p>Two implementation details that are easy to miss: the optical flow at line 1 runs &lt;strong>backwards&lt;/strong>, tracking the current frame&amp;rsquo;s points into the previous frame to reconstruct correspondences, and the fundamental matrix uses the &lt;strong>seven-point&lt;/strong> method inside RANSAC rather than the more common normalised eight-point variant.&lt;/p>
&lt;h2 id="detector">Detector&lt;/h2>
&lt;p>Detection runs an &lt;strong>SSD with a MobileNetV3 backbone under NCNN&lt;/strong>, Tencent&amp;rsquo;s mobile CPU inference framework. That choice is what makes the real-time claim plausible without a GPU in the loop — the whole point of SG-SLAM over heavier semantic-SLAM systems is that it targets mobile platforms. The tracking thread computes its geometric quantities, blocks on the detector&amp;rsquo;s 2-D result for that frame, then runs rejection and tracks; only surviving features reach local mapping, loop closing and full bundle adjustment, all of which are unmodified ORB-SLAM2.&lt;/p>
&lt;div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;align-items:start">
&lt;div>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Detection overlay with bounding boxes and class confidences" srcset="
/saikiran_juttu.github.io/project/sg-slam/detection_hu9192830305904395551.webp 400w,
/saikiran_juttu.github.io/project/sg-slam/detection_hu10414967818364573976.webp 760w,
/saikiran_juttu.github.io/project/sg-slam/detection_hu7677053416326043211.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/sg-slam/detection_hu9192830305904395551.webp"
width="368"
height="226"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>Detection on a live frame.&lt;/em>&lt;/p>
&lt;/div>
&lt;div>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Semantic object map with per-object 3D coordinates in RViz" srcset="
/saikiran_juttu.github.io/project/sg-slam/semantic-map_hu10489737656378413939.webp 400w,
/saikiran_juttu.github.io/project/sg-slam/semantic-map_hu13843709679552535452.webp 760w,
/saikiran_juttu.github.io/project/sg-slam/semantic-map_hu13383796692928516754.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/sg-slam/semantic-map_hu10489737656378413939.webp"
width="368"
height="233"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>Objects localised in 3-D with class labels.&lt;/em>&lt;/p>
&lt;/div>
&lt;/div>
&lt;p>The semantic mapping thread fuses the 2-D detections with per-keyframe point clouds generated from the depth images and camera poses, then extracts each object&amp;rsquo;s position and extent into a 3-D semantic object database. That database, a global OctoMap and the camera poses are all published over ROS for RViz — which is the real difference from plain ORB-SLAM2, whose output is a sparse cloud with no idea what anything is.&lt;/p>
&lt;hr>
&lt;h2 id="results">Results&lt;/h2>
&lt;p>Benchmark figures below are the published results from Cheng et al., reproduced here rather than independently measured. Metric is &lt;strong>ATE RMSE in metres&lt;/strong> — these are error &lt;em>reductions&lt;/em>, not accuracy gains.&lt;/p>
&lt;h3 id="tum-rgb-d">TUM RGB-D&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Sequence&lt;/th>
&lt;th>Dynamics&lt;/th>
&lt;th>ORB-SLAM2&lt;/th>
&lt;th>SG-SLAM&lt;/th>
&lt;th>Reduction&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>fr3/walking_static&lt;/code>&lt;/td>
&lt;td>high&lt;/td>
&lt;td>0.4032&lt;/td>
&lt;td>0.0079&lt;/td>
&lt;td>&lt;strong>98.03%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>fr3/walking_xyz&lt;/code>&lt;/td>
&lt;td>high&lt;/td>
&lt;td>0.6826&lt;/td>
&lt;td>0.0171&lt;/td>
&lt;td>&lt;strong>97.50%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>fr3/walking_rpy&lt;/code>&lt;/td>
&lt;td>high&lt;/td>
&lt;td>0.5396&lt;/td>
&lt;td>0.0326&lt;/td>
&lt;td>&lt;strong>93.95%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>fr3/walking_halfsphere&lt;/code>&lt;/td>
&lt;td>high&lt;/td>
&lt;td>0.4462&lt;/td>
&lt;td>0.0309&lt;/td>
&lt;td>&lt;strong>93.07%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>fr3/sitting_static&lt;/code>&lt;/td>
&lt;td>low&lt;/td>
&lt;td>0.0087&lt;/td>
&lt;td>0.0060&lt;/td>
&lt;td>&lt;strong>31.03%&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="bonn-rgb-d-dynamic-9-sequences">Bonn RGB-D Dynamic (9 sequences)&lt;/h3>
&lt;p>Best and worst of the set: &lt;code>synchronous2&lt;/code> improves 1.4069 → 0.0164 m (&lt;strong>98.83%&lt;/strong>), while &lt;code>synchronous&lt;/code> — nominally the same scene, different take — only reaches 1.1411 → 0.3262 m (&lt;strong>71.41%&lt;/strong>). The &lt;code>moving_nonobstructing_box&lt;/code> pair land around 71–79%, the &lt;code>crowd&lt;/code> and &lt;code>person_tracking&lt;/code> sequences 93–97%.&lt;/p>
&lt;h3 id="reading-the-numbers-honestly">Reading the numbers honestly&lt;/h3>
&lt;p>Two things about this table are worth saying out loud, because they&amp;rsquo;re the parts a results summary usually hides.&lt;/p>
&lt;p>&lt;strong>The 31% on &lt;code>sitting_static&lt;/code> isn&amp;rsquo;t a weak result, it&amp;rsquo;s the control.&lt;/strong> That sequence is low-dynamic — a person seated, barely moving. There is almost nothing for dynamic-feature rejection to remove, so the gain collapses to a third of what the walking sequences show. The rotational-drift table is starker still: 7.99% on the same sequence. That&amp;rsquo;s the expected shape of the result, and it&amp;rsquo;s evidence the mechanism is doing what it claims rather than just globally discarding features.&lt;/p>
&lt;p>&lt;strong>RMSE improves far more than the median does.&lt;/strong> On &lt;code>walking_static&lt;/code> the ATE RMSE drops 98% while the RPE median improvements sit at 43–53%. The gain is concentrated in &lt;em>catastrophic&lt;/em> frames — the ones where a person crossing the view wrecks the pose estimate entirely — not in the typical frame. That&amp;rsquo;s the right thing for a SLAM system to fix, since a single badly-corrupted keyframe propagates into the map, but it does mean &amp;ldquo;98% better&amp;rdquo; describes the tail, not the average.&lt;/p>
&lt;hr>
&lt;h2 id="where-it-breaks">Where it breaks&lt;/h2>
&lt;p>The failure mode falls straight out of Algorithm 1, and the paper is upfront about it: &lt;strong>an object moving along the epipolar line direction is invisible to this test.&lt;/strong> Its features produce a point-to-epipolar-line distance of approximately zero despite genuinely moving, so line 10 accepts them as static. The semantic branch doesn&amp;rsquo;t save you either — multiplying a near-zero residual by a dynamic weight still clears the threshold. In practice that means someone walking directly toward or away from the camera is much harder to reject than someone crossing the view.&lt;/p>
&lt;p>The other open item is semantic map precision. Object extents come from thresholding a depth-derived point cloud inside a 2-D box, which is coarse — good enough to say &amp;ldquo;there is a monitor at roughly here&amp;rdquo;, not good enough for manipulation.&lt;/p>
&lt;h2 id="stack">Stack&lt;/h2>
&lt;p>C++ (ORB-SLAM2 core), Python, Ubuntu 18.04, ROS Melodic, NCNN, OpenCV, OctoMap, RViz. Evaluated on TUM RGB-D and the Bonn RGB-D Dynamic dataset.&lt;/p></description></item><item><title>Dead Reckoning with IMU &amp; GPS in Vehicle Navigation</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/project/dead-reckoning/</link><pubDate>Tue, 09 Apr 2024 00:00:00 +0000</pubDate><guid>https://juttu-s.github.io/saikiran_juttu.github.io/project/dead-reckoning/</guid><description>&lt;p>Take away the GPS fix and a vehicle has to work out where it is from acceleration and rotation alone. This project does that end to end on real data — instrument a car, calibrate the magnetometer properly, fuse a heading estimate, integrate forward velocity, and reconstruct the trajectory — then compares the result against GPS to find out how long dead reckoning actually survives.&lt;/p>
&lt;p>The short answer: &lt;strong>about three minutes.&lt;/strong> After that it is off by the length of a city block.&lt;/p>
&lt;p>Coursework for &lt;strong>EECE5554: Robotics Sensing and Navigation&lt;/strong> at Northeastern.&lt;/p>
&lt;hr>
&lt;h2 id="setup">Setup&lt;/h2>
&lt;p>A &lt;strong>VectorNav VN-100&lt;/strong> IMU mounted inside the vehicle in a custom holder, deliberately aligned so its &lt;strong>x-axis points along the vehicle&amp;rsquo;s forward direction&lt;/strong> and the unit sits level. That alignment is doing real work later — it&amp;rsquo;s what lets &lt;code>accel_x&lt;/code> be treated as forward acceleration and &lt;code>accel_y&lt;/code> as lateral without a rotation. A magnetic-mount GPS puck went on the roof, both feeding one laptop over ROS.&lt;/p>
&lt;p>Two datasets:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Circles&lt;/strong> — five laps of the Ruggles station circle, recorded purely to excite the magnetometer through a full 360° of heading. This is calibration data, not navigation data.&lt;/li>
&lt;li>&lt;strong>Driving&lt;/strong> — a 2–3 km route around Boston with at least ten turns, roughly 1300 s, routed to avoid tunnels and underpasses so the GPS reference stays continuous.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="magnetometer-calibration">Magnetometer calibration&lt;/h2>
&lt;p>An uncalibrated magnetometer driven in a circle should trace a circle. It doesn&amp;rsquo;t, and the two ways it fails are visually distinguishable from the same scatter plot:&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Magnetometer X-Y scatter before and after calibration — the measured ring offset from the origin, the calibrated ring centred on it" srcset="
/saikiran_juttu.github.io/project/dead-reckoning/magnetometer-calibration_hu6564607695943048432.webp 400w,
/saikiran_juttu.github.io/project/dead-reckoning/magnetometer-calibration_hu4157646788420770913.webp 760w,
/saikiran_juttu.github.io/project/dead-reckoning/magnetometer-calibration_hu17823870522234568360.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/dead-reckoning/magnetometer-calibration_hu6564607695943048432.webp"
width="760"
height="573"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>Five laps of the Ruggles circle. Orange is raw, blue is corrected.&lt;/em>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The ring is off-centre&lt;/strong> → &lt;strong>hard iron&lt;/strong> distortion. A constant field offset from permanent magnets, ferrous structure or current-carrying conductors near the sensor. The raw ring here sits up and to the right of the origin, biased by roughly 0.27 G in x and 0.22 G in y. Correction is a per-axis bias subtraction that recentres the cluster.&lt;/li>
&lt;li>&lt;strong>The ring is deformed&lt;/strong> → &lt;strong>soft iron&lt;/strong> distortion. Non-uniform scaling and rotation from nearby ferrous material. You can see it in both rings as a distinct kink near the +x side — the shape isn&amp;rsquo;t circular and no amount of translation fixes that. Correction is a matrix that scales and rotates the data back toward a circle.&lt;/li>
&lt;/ul>
&lt;p>The parameters come from a least-squares ellipse fit to the circle-driving data, applied as bias subtraction followed by the correction matrix. Being able to read &lt;em>which&lt;/em> distortion you have off the plot before fixing either is the useful part of this exercise — the two have different causes and different remedies, and the geometry tells you which you&amp;rsquo;re looking at.&lt;/p>
&lt;h2 id="heading">Heading&lt;/h2>
&lt;p>Neither heading source is usable alone. The magnetometer gives absolute heading but is noisy and sensitive to the vehicle&amp;rsquo;s own electrical and magnetic environment. The gyro is clean over short intervals but integrates its bias without bound — the raw integrated yaw here climbs to about &lt;strong>+405°&lt;/strong> by t ≈ 560 s and plateaus near 300°, which is meaningless as an absolute heading.&lt;/p>
&lt;p>A complementary filter takes the half of each signal that&amp;rsquo;s trustworthy:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>0.5 Hz low-pass&lt;/strong> on magnetometer yaw — keeps the slow-moving absolute reference, discards the noise&lt;/li>
&lt;li>&lt;strong>0.1 Hz high-pass&lt;/strong> on gyro yaw — keeps short-term rate fidelity, discards the drift&lt;/li>
&lt;li>blended with coefficient &lt;strong>0.98&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Four-panel comparison: low-passed magnetometer yaw, high-passed gyro yaw, complementary filter output, and the VN-100&amp;rsquo;s own onboard heading estimate" srcset="
/saikiran_juttu.github.io/project/dead-reckoning/heading-fusion_hu16052623581733354419.webp 400w,
/saikiran_juttu.github.io/project/dead-reckoning/heading-fusion_hu9034352427183490476.webp 760w,
/saikiran_juttu.github.io/project/dead-reckoning/heading-fusion_hu864362406907695216.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/dead-reckoning/heading-fusion_hu16052623581733354419.webp"
width="760"
height="372"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>The two filtered branches, the fused output, and the VN-100&amp;rsquo;s internal estimate for comparison.&lt;/em>&lt;/p>
&lt;hr>
&lt;h2 id="forward-velocity-and-the-number-that-gave-the-game-away">Forward velocity, and the number that gave the game away&lt;/h2>
&lt;p>Integrating raw accelerometer output produces nonsense. The raw forward-velocity trace peaks around &lt;strong>+35 m/s&lt;/strong> and dips to &lt;strong>−15 m/s&lt;/strong> — the car did neither. That&amp;rsquo;s accelerometer bias being integrated into an unbounded ramp, plus road vibration and the gravity component that any unnoticed slope injects into the forward axis.&lt;/p>
&lt;p>Two corrections bring it into physical range: a high-pass filter on forward acceleration to strip the low-frequency bias, then clamps — negative velocities to zero, plus an upper bound. That gets the adjusted trace to a plausible &lt;strong>0–11 m/s&lt;/strong>, against GPS-derived velocity peaking at 12.7 m/s.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Raw integrated forward velocity against the filtered and clamped version" srcset="
/saikiran_juttu.github.io/project/dead-reckoning/forward-velocity_hu6766549600153078957.webp 400w,
/saikiran_juttu.github.io/project/dead-reckoning/forward-velocity_hu2268536860800196880.webp 760w,
/saikiran_juttu.github.io/project/dead-reckoning/forward-velocity_hu229241971526026250.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/dead-reckoning/forward-velocity_hu6766549600153078957.webp"
width="760"
height="398"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>Blue: integrate the raw signal and the car reaches 35 m/s and reverses. Orange: after high-pass filtering and clamping.&lt;/em>&lt;/p>
&lt;p>Then aligning the dead-reckoned track to GPS required scaling the IMU distances by &lt;strong>2.25×&lt;/strong>.&lt;/p>
&lt;p>That factor is the most informative number in the whole project, and it isn&amp;rsquo;t a calibration constant — it&amp;rsquo;s a symptom. A 2.25× scale error means the integrated velocity was systematically &lt;strong>less than half&lt;/strong> the true speed, and both corrections above are implicated. The high-pass filter that removed accelerometer bias also removed genuine low-frequency acceleration, because a car accelerating gently from a light and a slowly-drifting bias occupy the same part of the spectrum. And clamping negative velocity to zero doesn&amp;rsquo;t just discard drift — it &lt;strong>rectifies&lt;/strong> the signal, converting every real deceleration the drifting integrator pushed below zero into &amp;ldquo;stationary&amp;rdquo;. Distance travelled comes out short in both cases.&lt;/p>
&lt;p>&lt;strong>The filter that fixed the drift destroyed the scale.&lt;/strong> Getting both would need the bias estimated as a state rather than filtered out — which is the argument for a Kalman filter over hand-tuned Butterworths, and the direction I&amp;rsquo;d take this next.&lt;/p>
&lt;h2 id="trajectory">Trajectory&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Dead-reckoned trajectory beside the GPS ground track, both in Easting/Northing metres" srcset="
/saikiran_juttu.github.io/project/dead-reckoning/trajectories_hu12573546119952805973.webp 400w,
/saikiran_juttu.github.io/project/dead-reckoning/trajectories_hu11079378006867108752.webp 760w,
/saikiran_juttu.github.io/project/dead-reckoning/trajectories_hu14489892853533858604.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/dead-reckoning/trajectories_hu12573546119952805973.webp"
width="760"
height="347"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>Dead-reckoned track (left) against GPS (right). The shape survives; the extent doesn&amp;rsquo;t.&lt;/em>&lt;/p>
&lt;p>Projecting forward velocity onto the fused heading and integrating gives the track above, aligned to GPS by matching start coordinates and the first straight segment.&lt;/p>
&lt;ul>
&lt;li>The two agree to within about &lt;strong>2 m for the first 150–200 s&lt;/strong>&lt;/li>
&lt;li>After that they diverge, reaching roughly &lt;strong>400 m&lt;/strong> of displacement error by the end of the ~2–3 km route&lt;/li>
&lt;/ul>
&lt;p>Dead reckoning worked best exactly where you&amp;rsquo;d expect — early, on straight, level road. It degraded with each successive turn, and the two causes are &lt;strong>gyro bias instability&lt;/strong> feeding heading error into every subsequent position update, and &lt;strong>unanticipated road slopes&lt;/strong> tilting gravity onto the forward axis where it gets integrated as acceleration.&lt;/p>
&lt;p>One honest wrinkle in the alignment: the IMU track was rotated 14° and the GPS track 25°, in opposite service of making the first straight segments agree. Rotating &lt;em>both&lt;/em> by &lt;em>different&lt;/em> amounts means the result no longer references true north — a single rotation of the difference would have been the defensible choice.&lt;/p>
&lt;h2 id="the-takeaway">The takeaway&lt;/h2>
&lt;p>There&amp;rsquo;s a usable operating envelope here and it&amp;rsquo;s narrow. Roughly &lt;strong>150–200 seconds of GPS outage&lt;/strong> is survivable at 2 m accuracy, which is enough to cross a tunnel or an urban canyon. It is nowhere near enough to navigate on, and the failure is not graceful — error grows superlinearly as heading error compounds. Any real system needs periodic absolute fixes, and the interesting engineering question is how sparse those fixes can be, not whether you need them.&lt;/p>
&lt;h2 id="stack">Stack&lt;/h2>
&lt;p>ROS (rosbag, sensor nodes), Python, SciPy (&lt;code>butter&lt;/code>/&lt;code>filtfilt&lt;/code>, least-squares fitting), matplotlib, UTM projection. Hardware: VectorNav VN-100, magnetic-mount GPS puck, Northeastern&amp;rsquo;s instrumented &amp;ldquo;nuance&amp;rdquo; vehicle.&lt;/p></description></item></channel></rss>