<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>OpenCV | Saikiran Juttu | Robotics Portfolio</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/tags/opencv/</link><atom:link href="https://juttu-s.github.io/saikiran_juttu.github.io/tags/opencv/index.xml" rel="self" type="application/rss+xml"/><description>OpenCV</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Thu, 07 Nov 2024 00:00:00 +0000</lastBuildDate><image><url>https://juttu-s.github.io/saikiran_juttu.github.io/media/icon_hu7729264130191091259.png</url><title>OpenCV</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/tags/opencv/</link></image><item><title>Sparse 3D Reconstruction and Bundle Adjustment</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/project/sparse-reconstruction/</link><pubDate>Thu, 07 Nov 2024 00:00:00 +0000</pubDate><guid>https://juttu-s.github.io/saikiran_juttu.github.io/project/sparse-reconstruction/</guid><description>&lt;p>This project implements a full Structure from Motion (SfM) pipeline on a &lt;strong>Buddha statue&lt;/strong> using a sequence of &lt;strong>24 grayscale images&lt;/strong>. It combines feature detection, epipolar geometry, camera pose recovery, triangulation, and bundle adjustment.&lt;/p>
&lt;hr>
&lt;h3 id="dataset">Dataset&lt;/h3>
&lt;ul>
&lt;li>24 images of a wooden Buddha statue captured at different angles&lt;/li>
&lt;li>Enhanced using &lt;strong>CLAHE (Contrast Limited Adaptive Histogram Equalization)&lt;/strong>&lt;/li>
&lt;li>Features extracted using &lt;strong>SIFT&lt;/strong> with custom parameters&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="pipeline-overview">Pipeline Overview&lt;/h3>
&lt;h4 id="1-image-preprocessing">1. Image Preprocessing&lt;/h4>
&lt;p>Using CLAHE improves contrast on low-texture surfaces like carved wood.&lt;/p>
&lt;div style="text-align: center;">
&lt;img src="processed.png" width="1000">
&lt;/div>
&lt;hr>
&lt;h4 id="2-sift-feature-detection">2. SIFT Feature Detection&lt;/h4>
&lt;ul>
&lt;li>Applied to all 24 images&lt;/li>
&lt;li>Used &lt;strong>BFMatcher&lt;/strong> with ratio test&lt;/li>
&lt;li>Matches filtered via &lt;strong>RANSAC&lt;/strong> for outlier rejection&lt;/li>
&lt;/ul>
&lt;div style="text-align: center;">
&lt;img src="features.png" width="1000">
&lt;/div>
&lt;hr>
&lt;h4 id="3-essential-matrix--pose-recovery">3. Essential Matrix &amp;amp; Pose Recovery&lt;/h4>
&lt;ul>
&lt;li>Computed Essential matrix using calibrated camera matrix&lt;/li>
&lt;li>Used &lt;code>cv2.recoverPose()&lt;/code> to derive relative rotation and translation between views&lt;/li>
&lt;li>Built a chain of camera poses from image 0 onward&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="4-triangulation">4. Triangulation&lt;/h4>
&lt;ul>
&lt;li>3D points computed from pixel correspondences using &lt;code>cv2.triangulatePoints()&lt;/code>&lt;/li>
&lt;li>All 3D points stored in homogeneous form&lt;/li>
&lt;li>Colored and visualized using Plotly&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h4 id="5-bundle-adjustment-with-gtsam">5. Bundle Adjustment with GTSAM&lt;/h4>
&lt;ul>
&lt;li>Built a factor graph with:
&lt;ul>
&lt;li>Camera pose priors&lt;/li>
&lt;li>Between factors from pose transitions&lt;/li>
&lt;li>Projection factors from 2D-3D matches&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Used &lt;code>Levenberg-MarquardtOptimizer&lt;/code> for refinement&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="results">Results&lt;/h3>
&lt;h4 id="initial-3d-trajectory">Initial 3D Trajectory&lt;/h4>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Initial Trajectory" srcset="
/saikiran_juttu.github.io/project/sparse-reconstruction/Initial_plot_hu12090084634136375331.webp 400w,
/saikiran_juttu.github.io/project/sparse-reconstruction/Initial_plot_hu6971695684859321629.webp 760w,
/saikiran_juttu.github.io/project/sparse-reconstruction/Initial_plot_hu11908849571792030370.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/sparse-reconstruction/Initial_plot_hu12090084634136375331.webp"
width="760"
height="282"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h4 id="optimized-3d-trajectory-after-bundle-adjustment">Optimized 3D Trajectory after Bundle Adjustment&lt;/h4>
&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/sparse-reconstruction/Optimized_plot_hu1365288965832748826.webp 400w,
/saikiran_juttu.github.io/project/sparse-reconstruction/Optimized_plot_hu5432368712792554619.webp 760w,
/saikiran_juttu.github.io/project/sparse-reconstruction/Optimized_plot_hu6617160073620067270.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/sparse-reconstruction/Optimized_plot_hu1365288965832748826.webp"
width="760"
height="282"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;ul>
&lt;li>Average reprojection error reduced by ~15%&lt;/li>
&lt;li>Landmark cloud tightened around object geometry&lt;/li>
&lt;li>Rotation drift corrected with global optimization&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="tools--libraries">Tools &amp;amp; Libraries&lt;/h3>
&lt;ul>
&lt;li>OpenCV (SIFT, RANSAC, triangulation)&lt;/li>
&lt;li>NumPy, Matplotlib, Plotly&lt;/li>
&lt;li>GTSAM (factor graph + BA)&lt;/li>
&lt;li>Python&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h3 id="resources">Resources&lt;/h3>
&lt;ul>
&lt;li>🔗 &lt;a href="https://github.com/juttu-s/Sparse-3D-Reconstruction" target="_blank" rel="noopener">GitHub Repo&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>This project demonstrates a scalable pipeline for SfM using minimal dependencies. It serves as a foundation for integrating real-time VIO or stereo SLAM on embedded platforms.&lt;/p></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>Real-Time 2D Object Recognition with Feature Matching</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/project/object-recognition/</link><pubDate>Sat, 30 Mar 2024 00:00:00 +0000</pubDate><guid>https://juttu-s.github.io/saikiran_juttu.github.io/project/object-recognition/</guid><description>&lt;p>Recognise objects on a tabletop from a live webcam, using classical computer vision and nothing else — no learned features, no pretrained backbone. Camera overhead, dark objects on a white surface, everything computed per frame in C++.&lt;/p>
&lt;p>The constraint that made this interesting: most of the pipeline had to be written from scratch. Two of the first four stages were required to be; three ended up that way. The only OpenCV algorithm doing real work is connected-component labelling.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="A watch and a pen segmented with oriented bounding boxes, axis-of-least-central-moment arrows, and live feature values overlaid" srcset="
/saikiran_juttu.github.io/project/object-recognition/features-annotated_hu1285270646848479672.webp 400w,
/saikiran_juttu.github.io/project/object-recognition/features-annotated_hu4246342232475753791.webp 760w,
/saikiran_juttu.github.io/project/object-recognition/features-annotated_hu5357358603646478662.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/object-recognition/features-annotated_hu1285270646848479672.webp"
width="640"
height="480"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>Two objects at once. Blue boxes are the oriented bounding boxes, red arrows the axis of least central moment, cyan text the live feature values.&lt;/em>&lt;/p>
&lt;hr>
&lt;h2 id="pipeline">Pipeline&lt;/h2>
&lt;h3 id="thresholding-without-otsu">Thresholding, without Otsu&lt;/h3>
&lt;p>Rather than call a threshold function, the threshold is found by &lt;strong>2-means clustering on sampled pixel values&lt;/strong>. Sample the frame, converge two centroids — one settles on the dark object population, one on the light background — and put the threshold at their midpoint.&lt;/p>
&lt;p>The appeal is that it&amp;rsquo;s &lt;em>adaptive by construction&lt;/em>. As the lighting shifts, both centroids move and the threshold tracks them, without a hand-tuned constant anywhere. It&amp;rsquo;s a genuinely better fit for a live feed than a fixed cut, and it&amp;rsquo;s about fifteen lines of code.&lt;/p>
&lt;h3 id="morphological-cleanup">Morphological cleanup&lt;/h3>
&lt;p>The thresholded feed had holes in it — printed text and specular highlights on dark objects read as background. So: &lt;strong>dilation first to close the gaps, then erosion to remove the speckle&lt;/strong> the dilation amplified. Written by hand rather than called, and the ordering was driven by looking at the actual defect rather than reaching for a default.&lt;/p>
&lt;h3 id="segmentation">Segmentation&lt;/h3>
&lt;p>&lt;code>cv::connectedComponentsWithStats&lt;/code> labels the regions; components below a size threshold are dropped as noise. Survivors get distinct colours for display.&lt;/p>
&lt;h3 id="features-from-moments-up">Features, from moments up&lt;/h3>
&lt;p>Raw and central moments computed directly, then five descriptors per region:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Feature&lt;/th>
&lt;th>What it captures&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Centroid (x, y)&lt;/td>
&lt;td>Region position&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>θ&lt;/td>
&lt;td>Angle of the axis of least central moment — the object&amp;rsquo;s orientation&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Percent filled&lt;/td>
&lt;td>Region area ÷ oriented bounding box area&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bounding box ratio&lt;/td>
&lt;td>Oriented box aspect ratio&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Stored to CSV alongside a label typed at capture time, which makes the training set inspectable — you can open it and see why the classifier does what it does.&lt;/p>
&lt;h2 id="classification">Classification&lt;/h2>
&lt;p>Two classifiers over the same features:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Nearest neighbour&lt;/strong> on cumulative scaled Euclidean distance — closest labelled example wins.&lt;/li>
&lt;li>&lt;strong>k-NN with k = 4&lt;/strong> — take the four nearest, majority vote.&lt;/li>
&lt;/ul>
&lt;p>The k-NN version is the more robust of the two, and the reason is visible in the failure mode of the first. Nearest neighbour commits to a single best match, so when two classes differ only marginally in feature space, one noisy frame is enough to flip the decision. Requiring agreement among four neighbours means a single outlier can&amp;rsquo;t carry the vote.&lt;/p>
&lt;hr>
&lt;h2 id="results">Results&lt;/h2>
&lt;p>&lt;strong>11 object classes&lt;/strong>, roughly 30 labelled samples captured at varying positions and orientations: watch, pen, mobile, spoon, bracelet, earbuds box, pendrive, statue, controller, star, clutch. Over 15 trials across five classes, classification accuracy ran &lt;strong>93.33–100%&lt;/strong>.&lt;/p>
&lt;p>The more informative result is how well the shape features separate the classes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Feature&lt;/th>
&lt;th>Range across the 11 classes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Percent filled&lt;/td>
&lt;td>0.34 (bracelet — a hollow loop) → 0.97 (phone — a filled rectangle)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Bounding box ratio&lt;/td>
&lt;td>1.04 (near-square box) → 8.88 (pen)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Those two numbers do most of the discriminating, and they&amp;rsquo;re both scale- and rotation-invariant, which is the whole reason for computing orientation first and measuring the box &lt;em>after&lt;/em> aligning to it. Deliberately including several elongated objects — pen at 8.88, watch strap at 5.31, spoon at 3.99 — was what stress-tested it; anything can separate a pen from a phone, but separating a pen from a watch strap needs the percent-filled term to pull its weight.&lt;/p>
&lt;p>An extension pushed it to &lt;strong>multiple objects simultaneously&lt;/strong>, segmenting and classifying every region in the frame rather than assuming one object at a time.&lt;/p>
&lt;h2 id="honest-limitations">Honest limitations&lt;/h2>
&lt;p>&lt;strong>The feature vector includes absolute centroid position.&lt;/strong> Centroid x and y range from 165 to 484 px across the dataset, and feeding those into a scaled-Euclidean distance means &lt;em>where the object sits in frame&lt;/em> contributes to the class decision — in a system whose stated goal is translation invariance. The three shape descriptors are the invariant ones and are doing the real work; the centroid terms are a liability I&amp;rsquo;d drop.&lt;/p>
&lt;p>&lt;strong>Lighting drives everything.&lt;/strong> The 2-means threshold adapts, but it can only adapt to a bimodal scene. Introduce a shadow gradient across the white surface, or a mid-grey object, and the two-cluster assumption stops holding.&lt;/p>
&lt;p>&lt;strong>No timing was measured.&lt;/strong> The system runs interactively on a live feed, but there&amp;rsquo;s no frame-rate figure behind that, so I won&amp;rsquo;t claim one.&lt;/p>
&lt;h2 id="stack">Stack&lt;/h2>
&lt;p>C++, OpenCV 4, CMake. Thresholding, morphology and the entire moment/feature pipeline hand-written; &lt;code>connectedComponentsWithStats&lt;/code> for labelling. Training data self-collected via an in-app capture-and-label mode, with recording built in for demos.&lt;/p></description></item><item><title>Camera Calibration and Augmented Reality</title><link>https://juttu-s.github.io/saikiran_juttu.github.io/project/calibration/</link><pubDate>Mon, 18 Mar 2024 00:00:00 +0000</pubDate><guid>https://juttu-s.github.io/saikiran_juttu.github.io/project/calibration/</guid><description>&lt;p>Everything in marker-based AR rests on one number: how well you know the camera. This project works through that chain end to end in C++/OpenCV — detect a calibration target, solve for intrinsics, recover live pose, and project virtual geometry that stays locked to the target as the camera moves.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="A green wireframe object rendered over a chessboard with rainbow corner polylines and RGB origin axes" srcset="
/saikiran_juttu.github.io/project/calibration/virtual-object_hu5133569573855807652.webp 400w,
/saikiran_juttu.github.io/project/calibration/virtual-object_hu6246078329211090082.webp 760w,
/saikiran_juttu.github.io/project/calibration/virtual-object_hu6792843897147638943.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/calibration/virtual-object_hu5133569573855807652.webp"
width="474"
height="411"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>The virtual object holding its pose against the board.&lt;/em>&lt;/p>
&lt;hr>
&lt;h2 id="calibration">Calibration&lt;/h2>
&lt;p>A &lt;strong>9×6 chessboard giving 54 interior corners&lt;/strong>, detected on a live stream with sub-pixel refinement. Pressing &lt;code>s&lt;/code> banks the current frame&amp;rsquo;s corner locations along with their corresponding 3-D world points; &lt;code>cv::calibrateCamera&lt;/code> then solves for intrinsics and distortion. Fewer than five frames and it refuses — the parameters aren&amp;rsquo;t determined.&lt;/p>
&lt;p>Recovered intrinsics for the laptop camera, from an initial guess of nothing more than a principal point at the image centre:&lt;/p>
$$K = \begin{bmatrix} 611.41 &amp; 0 &amp; 311.84 \\ 0 &amp; 613.70 &amp; 236.82 \\ 0 &amp; 0 &amp; 1 \end{bmatrix}$$&lt;p>&lt;strong>Final reprojection error: 0.545 px.&lt;/strong> Intrinsics and distortion coefficients are written out as OpenCV YAML so later runs skip calibration entirely.&lt;/p>
&lt;h3 id="two-cameras-one-surprise">Two cameras, one surprise&lt;/h3>
&lt;p>Running the same procedure on an external USB webcam gave a notably better fit:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Camera&lt;/th>
&lt;th>fx, fy&lt;/th>
&lt;th>cx, cy&lt;/th>
&lt;th>Reprojection error&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Laptop integrated&lt;/td>
&lt;td>611.41, 613.70&lt;/td>
&lt;td>311.84, 236.82&lt;/td>
&lt;td>0.545 px&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>External USB webcam&lt;/td>
&lt;td>690.48, 693.37&lt;/td>
&lt;td>344.83, 239.23&lt;/td>
&lt;td>&lt;strong>0.150 px&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The webcam calibrates about &lt;strong>3.6× better&lt;/strong> — and the interesting part is that it does so while having &lt;em>substantially larger&lt;/em> distortion coefficients (k1 = −0.446 against −0.215, k3 = −4.44 against −1.76). More distortion, better fit. Those aren&amp;rsquo;t contradictory: the reprojection error measures how well the &lt;em>model&lt;/em> explains the observations, not how mild the lens is. A strongly distorted lens whose distortion is well-described by the radial polynomial will beat a milder lens whose residual behaviour the model can&amp;rsquo;t represent. Cheap wide optics can calibrate beautifully; the question is whether the model matches the physics.&lt;/p>
&lt;h2 id="pose">Pose&lt;/h2>
&lt;p>With intrinsics fixed, &lt;code>solvePnP&lt;/code> returns rotation and translation per frame. The sanity check is nicer than eyeballing an overlay — translate the camera to the right and watch the numbers:&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">tvec [7.378, 6.753, 40.531]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tvec [7.627, 6.719, 40.485]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tvec [7.759, 6.711, 40.504]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tvec [8.070, 6.713, 40.486]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>X climbs monotonically, 7.378 → 8.070, while Y holds near 6.71 and Z near 40.49. That&amp;rsquo;s exactly right for pure lateral motion, and it&amp;rsquo;s a check on the physical meaning of the solution rather than on whether the render happens to look plausible.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Projected 3D axes anchored at the board origin with all 54 corners detected and linked" srcset="
/saikiran_juttu.github.io/project/calibration/projected-axes_hu14538787005341851264.webp 400w,
/saikiran_juttu.github.io/project/calibration/projected-axes_hu3207897690070648796.webp 760w,
/saikiran_juttu.github.io/project/calibration/projected-axes_hu17627915606788624727.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/calibration/projected-axes_hu14538787005341851264.webp"
width="633"
height="388"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>Detected corners and the projected 3-D axes at the board origin.&lt;/em>&lt;/p>
&lt;p>&lt;code>cv::projectPoints&lt;/code> then puts 3-D world points back onto the image plane — the board corners, RGB axes at the origin, and a virtual object defined as &lt;strong>15 points forming an inverted C&lt;/strong>, connected by lines drawn between their projections.&lt;/p>
&lt;hr>
&lt;h2 id="extensions">Extensions&lt;/h2>
&lt;p>&lt;strong>Making the marker stop looking like a marker.&lt;/strong> The strongest result here: find the board&amp;rsquo;s four extreme corners, warp an arbitrary image into that quadrilateral, and render the virtual object on top of the warped result. The chessboard is still doing all the pose work underneath, but it&amp;rsquo;s no longer visible — which is the difference between a calibration demo and something you&amp;rsquo;d actually want in a scene.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="An arbitrary image warped over the chessboard region with the virtual object drawn on top" srcset="
/saikiran_juttu.github.io/project/calibration/target-overlay_hu10769559096996454137.webp 400w,
/saikiran_juttu.github.io/project/calibration/target-overlay_hu16920568668342420622.webp 760w,
/saikiran_juttu.github.io/project/calibration/target-overlay_hu14363001692669884525.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/calibration/target-overlay_hu10769559096996454137.webp"
width="491"
height="395"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>The board&amp;rsquo;s squares fully replaced, virtual geometry still tracking correctly.&lt;/em>&lt;/p>
&lt;p>&lt;strong>Two boards at once.&lt;/strong> A (9,6) and a (6,6) board handled in a single frame, each solved for pose independently and each rendered with its own coloured pyramid. It degrades gracefully — remove one board and the other keeps tracking.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Two checkerboards of different sizes tracked simultaneously, each with its own coloured wireframe pyramid" srcset="
/saikiran_juttu.github.io/project/calibration/multi-board_hu16617972295414141342.webp 400w,
/saikiran_juttu.github.io/project/calibration/multi-board_hu10299970603627648674.webp 760w,
/saikiran_juttu.github.io/project/calibration/multi-board_hu7114270305068571192.webp 1200w"
src="https://juttu-s.github.io/saikiran_juttu.github.io/saikiran_juttu.github.io/project/calibration/multi-board_hu16617972295414141342.webp"
width="656"
height="445"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;em>Independent pose solutions for two different board geometries in one frame.&lt;/em>&lt;/p>
&lt;p>&lt;strong>Pre-recorded video mode.&lt;/strong> Pressing &lt;code>p&lt;/code> runs the whole pipeline over recorded footage instead of the live feed, inserting virtual objects into video after the fact.&lt;/p>
&lt;h2 id="toward-markerless">Toward markerless&lt;/h2>
&lt;p>&lt;code>cv::cornerHarris&lt;/code> was added to explore features that don&amp;rsquo;t require a known target, swept over threshold, block size, aperture size and the Harris free parameter &lt;em>k&lt;/em>. Raising the threshold from 150 to 200 visibly thins the detections, dropping the finer corners first.&lt;/p>
&lt;p>Detection is where this stopped, and it&amp;rsquo;s worth being precise about that: &lt;strong>the markerless pipeline was designed, not built.&lt;/strong> Going from Harris corners to a pose estimate needs frame-to-frame correspondence — SIFT or SURF descriptors matched across frames, then a transformation recovered from the matches, then projection using that pose. That&amp;rsquo;s the natural continuation and it isn&amp;rsquo;t implemented here.&lt;/p>
&lt;h2 id="a-note-on-the-target">A note on the target&lt;/h2>
&lt;p>The chessboard in these figures is being &lt;strong>displayed on a phone screen&lt;/strong> rather than printed. Convenient, and it never creases — but it&amp;rsquo;s a glossy emissive surface, which introduces specular highlights and probably accounts for some of the laptop camera&amp;rsquo;s worse reprojection error. A matte printed target on rigid backing is the better choice if the calibration itself is what matters.&lt;/p>
&lt;h2 id="stack">Stack&lt;/h2>
&lt;p>C++, OpenCV 4, CMake, on Windows and Ubuntu. &lt;code>calibrateCamera&lt;/code>, &lt;code>solvePnP&lt;/code>, &lt;code>projectPoints&lt;/code>, &lt;code>cornerHarris&lt;/code>; intrinsics persisted as OpenCV YAML. Two cameras: a laptop integrated camera and an external USB webcam.&lt;/p></description></item></channel></rss>