<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>SLAM | Saikiran Juttu | Robotics Portfolio</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/tags/slam/</link><atom:link href="https://juttu-s.github.io/saikiran_juttu.github.io/tags/slam/index.xml" rel="self" type="application/rss+xml"/><description>SLAM</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Thu, 01 May 2025 00:00:00 +0000</lastBuildDate><image><url>https://juttu-s.github.io/saikiran_juttu.github.io/media/icon_hu7729264130191091259.png</url><title>SLAM</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/tags/slam/</link></image><item><title>Robotics Engineer Co-op</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/details/robotics-coop/</link><pubDate>Thu, 01 May 2025 00:00:00 +0000</pubDate><guid>https://juttu-s.github.io/saikiran_juttu.github.io/details/robotics-coop/</guid><description>&lt;h2 id="overview">Overview&lt;/h2>
&lt;p>Worked at Northeastern University on:&lt;/p>
&lt;hr>
&lt;h3 id="sensor-integration">Sensor Integration&lt;/h3>
&lt;p>Integrated &lt;strong>3D LiDAR and Intel RealSense&lt;/strong> sensors with the &lt;strong>Scout Mini Rover&lt;/strong> via CAN protocol in ROS 2, deployed on &lt;strong>Jetson AGX Orin&lt;/strong> for synchronized perception, sensor fusion, and reliable HW–SW communication.&lt;/p>
&lt;p>&lt;strong>Physical Setup&lt;/strong>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/uploads/Robot.jpeg" alt="Scout Mini Rover Setup" loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h3 id="slam-implementation">SLAM Implementation&lt;/h3>
&lt;p>Implemented ROS 2-based &lt;strong>SLAM systems&lt;/strong> using:&lt;/p>
&lt;ul>
&lt;li>RTAB-Map&lt;/li>
&lt;li>LIO-SAM&lt;/li>
&lt;li>Visual-Inertial Odometry (VIO)&lt;/li>
&lt;li>SLAM Toolbox&lt;br>
In C++/Python with Nav2 for improved localization in GPS-denied environments.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>RTAB-Map Demo:&lt;/strong>&lt;/p>
&lt;video controls width="100%" muted autoplay loop>
&lt;source src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/uploads/RTAB_demo.mp4" type="video/mp4">
&lt;/video>
&lt;p>&lt;strong>LIO-SAM Screenshot:&lt;/strong>&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/uploads/lio_sam.png" alt="LIO-SAM Visualization" loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;hr>
&lt;h3 id="uav-motion-planning">UAV Motion Planning&lt;/h3>
&lt;p>Developed &lt;strong>waypoint navigation and motion planning&lt;/strong> pipelines for &lt;strong>Crazyflie and DJI Tello&lt;/strong> using the &lt;strong>OptiTrack motion capture system&lt;/strong>.&lt;/p>
&lt;ul>
&lt;li>Automated takeoff&lt;/li>
&lt;li>Precision landing&lt;/li>
&lt;li>Closed-loop control with state estimation&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="semantic-perception-with-vlms">Semantic Perception with VLMs&lt;/h3>
&lt;p>Exploring integration of &lt;strong>Vision-Language Models (VLMs)&lt;/strong> to:&lt;/p>
&lt;ul>
&lt;li>Enhance indoor semantic navigation&lt;/li>
&lt;li>Reduce localization drift&lt;/li>
&lt;li>Enable autonomous scene understanding&lt;/li>
&lt;/ul></description></item><item><title>Photo Mosaicking of Low-Contrast Underwater Images</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/project/photo-mosaicking/</link><pubDate>Sat, 05 Oct 2024 00:00:00 +0000</pubDate><guid>https://juttu-s.github.io/saikiran_juttu.github.io/project/photo-mosaicking/</guid><description>&lt;p>This project implements a full photo mosaicking and optimization pipeline using low-contrast underwater images from the &lt;strong>Skerki Bank Roman shipwreck&lt;/strong> dataset. The approach registers both sequential and non-sequential images using SIFT and RANSAC, computes affine transformations, and optimizes a global trajectory using GTSAM.&lt;/p>
&lt;hr>
&lt;h3 id="pipeline-breakdown">Pipeline Breakdown&lt;/h3>
&lt;h4 id="clahe-image-enhancement">CLAHE Image Enhancement&lt;/h4>
&lt;ul>
&lt;li>Applies histogram equalization to improve contrast and enhance keypoints.&lt;/li>
&lt;li>OpenCV CLAHE was used on each grayscale frame.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">clahe&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">cv2&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">createCLAHE&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">clipLimit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mf">2.0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">tileGridSize&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">8&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">clahe_image&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">clahe&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">apply&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">gray_image&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h4 id="sift-feature-detection">SIFT Feature Detection&lt;/h4>
&lt;ul>
&lt;li>Detected keypoints using tuned SIFT settings:
&lt;ul>
&lt;li>&lt;code>nfeatures=5000&lt;/code>, &lt;code>contrastThreshold=0.025&lt;/code>, &lt;code>nOctaveLayers=8&lt;/code>, &lt;code>sigma=1.5&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">sift&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">cv2&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">SIFT_create&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">...&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">kp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">desc&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">sift&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">detectAndCompute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">image&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h4 id="feature-matching--ransac-filtering">Feature Matching + RANSAC Filtering&lt;/h4>
&lt;ul>
&lt;li>Matched descriptors using Brute-Force Matcher + Lowe’s ratio test.&lt;/li>
&lt;li>Applied &lt;code>cv2.estimateAffine2D&lt;/code> with RANSAC to compute and refine transformation.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">matches&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">bf&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">knnMatch&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">des1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">des2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">k&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">good&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">m&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">m&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">n&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">matches&lt;/span> &lt;span class="k">if&lt;/span> &lt;span class="n">m&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">distance&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="mf">0.75&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">n&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">distance&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">H&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">mask&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">cv2&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">estimateAffine2D&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">pts1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">pts2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">method&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">cv2&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">RANSAC&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h4 id="pose-graph-construction-gtsam">Pose Graph Construction (GTSAM)&lt;/h4>
&lt;ul>
&lt;li>Built a factor graph using all non-repeating image pairs.&lt;/li>
&lt;li>Relative poses (affine transforms) were added as edges.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">graph&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">add&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BetweenFactorPose2&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">i1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">i2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">T_ij&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">noise_model&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="initial-trajectoryplot_beforepng">
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Initial Trajectory" srcset="
/saikiran_juttu.github.io/project/photo-mosaicking/plot_before_hu6875028771095386571.webp 400w,
/saikiran_juttu.github.io/project/photo-mosaicking/plot_before_hu3546410195309766098.webp 760w,
/saikiran_juttu.github.io/project/photo-mosaicking/plot_before_hu3825931163024726418.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/photo-mosaicking/plot_before_hu6875028771095386571.webp"
width="571"
height="455"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/h2>
&lt;h4 id="global-bundle-adjustment">Global Bundle Adjustment&lt;/h4>
&lt;ul>
&lt;li>Used GTSAM’s Levenberg-Marquardt optimizer to refine global poses.&lt;/li>
&lt;li>Corrects drift and adjusts poses to minimize total residual error.&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">optimizer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">gtsam&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">LevenbergMarquardtOptimizer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">graph&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">initial_estimate&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">optimizer&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">optimize&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Optimized Trajectory" srcset="
/saikiran_juttu.github.io/project/photo-mosaicking/plot_after_hu16779151673749428898.webp 400w,
/saikiran_juttu.github.io/project/photo-mosaicking/plot_after_hu14280678253068865284.webp 760w,
/saikiran_juttu.github.io/project/photo-mosaicking/plot_after_hu13790384701387224375.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/photo-mosaicking/plot_after_hu16779151673749428898.webp"
width="580"
height="455"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;hr>
&lt;h3 id="techniques-used">Techniques Used&lt;/h3>
&lt;ul>
&lt;li>Image normalization + CLAHE&lt;/li>
&lt;li>SIFT feature detection and matching&lt;/li>
&lt;li>RANSAC for outlier rejection&lt;/li>
&lt;li>Homography estimation using Levenberg–Marquardt&lt;/li>
&lt;li>Graph construction (GTSAM)&lt;/li>
&lt;li>Loop closure detection&lt;/li>
&lt;li>Pose optimization&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="results">Results&lt;/h3>
&lt;ul>
&lt;li>Successfully registered both sequential and non-sequential image pairs&lt;/li>
&lt;li>Constructed optimized pose graphs for 6 and 29 image subsets&lt;/li>
&lt;li>Achieved a ~20% improvement in alignment after bundle adjustment&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="-related-files">📁 Related Files&lt;/h3>
&lt;ul>
&lt;li>🔗 &lt;a href="https://github.com/juttu-s/photo-mosaicking-skerki" target="_blank" rel="noopener">GitHub Repository&lt;/a>&lt;/li>
&lt;li>📁 &lt;a href="https://drive.google.com/drive/folders/1AtvT65txGIgAG23NRs3EkvDET036a81O" target="_blank" rel="noopener">Skerki Dataset Reference (Google Drive)&lt;/a>&lt;/li>
&lt;li>📓 &lt;a href="https://juttu-s.github.io/saikiran_juttu.github.io/files/Part1_and_2.ipynb">Project Notebook&lt;/a>&lt;/li>
&lt;li>📓 &lt;a href="https://juttu-s.github.io/saikiran_juttu.github.io/files/Part_3.ipynb">Extended Analysis&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="references">References&lt;/h3>
&lt;ul>
&lt;li>Pizarro &amp;amp; Singh (2003): &lt;em>Toward large-area mosaicing for underwater scientific applications.&lt;/em>&lt;/li>
&lt;li>Ballard et al. (1998, 2000): &lt;em>Roman shipwreck discovery using submersible tech.&lt;/em>&lt;/li>
&lt;/ul></description></item><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></channel></rss>