<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sean TU</title>
    <description>The latest articles on DEV Community by Sean TU (@sean-tu-).</description>
    <link>https://dev.to/sean-tu-</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4234638%2Fce6edb88-0353-4c79-945c-2d6e279db4ce.png</url>
      <title>DEV Community: Sean TU</title>
      <link>https://dev.to/sean-tu-</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sean-tu-"/>
    <language>en</language>
    <item>
      <title>Planar PnP Returns Two Poses — I Feed Their Disagreement Into the Kalman Filter Instead of Picking One</title>
      <dc:creator>Sean TU</dc:creator>
      <pubDate>Mon, 09 Nov 2026 10:12:57 +0000</pubDate>
      <link>https://dev.to/sean-tu-/planar-pnp-returns-two-poses-i-feed-their-disagreement-into-the-kalman-filter-instead-of-picking-1ch3</link>
      <guid>https://dev.to/sean-tu-/planar-pnp-returns-two-poses-i-feed-their-disagreement-into-the-kalman-filter-instead-of-picking-1ch3</guid>
      <description>&lt;p&gt;I'm a first-year undergrad building a vision system for a campus robot competition run by our university's RoboMaster team. The robot tracks a planar visual marker (80×80 mm) with OpenCV's &lt;code&gt;solvePnPGeneric&lt;/code&gt; + &lt;code&gt;SOLVEPNP_IPPE&lt;/code&gt;, and the position feeds a 6D constant-velocity Kalman filter that drives the robot toward the marker.&lt;/p&gt;

&lt;p&gt;Planar PnP has a well-known problem: it returns &lt;strong&gt;two&lt;/strong&gt; valid pose solutions. The standard practice is to hard-pick the one with the lower reprojection error, or discard the measurement when the two are close. I tried something less binary — and I'm sharing it here because I couldn't find anyone doing exactly this, and I want to know if it holds up.&lt;/p&gt;

&lt;p&gt;Marker image: &lt;a href="https://imgur.com/a/oyeXVuP" rel="noopener noreferrer"&gt;https://imgur.com/a/oyeXVuP&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The method
&lt;/h2&gt;

&lt;p&gt;IPPE returns two solutions, each with a reprojection error (e0 for the nominal/min-error solution, e1 for the other) and a translation (t0, t1). Instead of picking one, I take the nominal translation t0 as the measurement and inflate the Kalman measurement noise covariance R by the spread between the two solutions:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S = [e0^2 / (e0^2 + e1^2)] * (t1 - t0)(t1 - t0)^T
R_eff = R_profile + S
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The weight approaches 1/2 when the two solutions are equally good (most ambiguous → largest inflation) and approaches 0 when the second solution is much worse (unambiguous → no inflation). &lt;br&gt;
The idea: when the two solutions disagree, tell the filter to trust this measurement less, instead of pretending we know which solution is correct.&lt;/p&gt;

&lt;p&gt;Orientation stays out of the filter entirely — yaw is taken raw from the pose per frame. The reason: a wrong-branch error is a discrete jump, not Gaussian noise, and a Kalman filter can't digest that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the data says so far
&lt;/h2&gt;

&lt;p&gt;One thing I checked: is the spread even real? One paper (Fourmy et al., Humanoids 2019) claims the two IPPE solutions share the same position and differ only in rotation — if that were true for our solver, S would be ~zero and the whole method vacuous.&lt;/p&gt;

&lt;p&gt;It isn't, at least for OpenCV's implementation. On 1088 frames of replay data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;|t1 − t0| median 18.2 mm (p90 28.2 mm), almost entirely in depth&lt;/li&gt;
&lt;li&gt;Szz median 1.87e-05 m² (~4.3 mm equivalent std), about 75× our initial Rzz&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the signal is real. But that's about all I can claim with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I don't know yet — this is unverified work
&lt;/h2&gt;

&lt;p&gt;To be clear about the status: &lt;strong&gt;this is a method, not a result.&lt;/strong&gt; The following are open, and I'm posting partly to get them challenged:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No prior work found.&lt;/strong&gt; I searched (35 queries, EN + CN) and found no paper or public implementation doing exactly this. The closest is Fourmy et al. 2019, which inflates rotation covariance in a factor graph when the two errors are close — but it still hard-picks a solution first, and the trigger is an error ratio, not a pose spread. "Not found" is not "does not exist."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The theoretical basis is an analogy, not a proof.&lt;/strong&gt; S structurally matches the between-component spread term of a two-component mixture covariance (law of total variance), but with three deviations: the coefficient should be c(1−c), not c (so S overestimates by up to 2× at full ambiguity — conservative, but ad hoc); the mean stays at t0 rather than the mixture mean; and the e²-ratio weight is not a Bayesian posterior. Related methods (PDAF/GPB) put the spread on the posterior P, not on the measurement R.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No controlled comparison yet.&lt;/strong&gt; I have not run fixed-R vs. ratio-test hard pick vs. R_profile+S on the same data. That experiment is planned alongside our R/Q calibration on real robot data — which hasn't happened yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you know prior work, see a flaw, or have a better way to handle planar PnP ambiguity in a tracking filter, I'd genuinely like to hear it. I also asked the same question on Robotics Stack Exchange: &lt;a href="https://robotics.stackexchange.com/questions/118663" rel="noopener noreferrer"&gt;https://robotics.stackexchange.com/questions/118663&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7b21k1q5hnuifmk675pn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7b21k1q5hnuifmk675pn.png" alt="The planar marker with the detection overlay" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>computervision</category>
      <category>cpp</category>
      <category>programming</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
