<?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: Jeff Li</title>
    <description>The latest articles on DEV Community by Jeff Li (@jeff_li_deee6ec68274e6300).</description>
    <link>https://dev.to/jeff_li_deee6ec68274e6300</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%2F4220500%2F982594ca-3cd7-4ffd-b814-30e1cb89a1c5.png</url>
      <title>DEV Community: Jeff Li</title>
      <link>https://dev.to/jeff_li_deee6ec68274e6300</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jeff_li_deee6ec68274e6300"/>
    <language>en</language>
    <item>
      <title>Measuring faces in the browser with MediaPipe: three things that broke</title>
      <dc:creator>Jeff Li</dc:creator>
      <pubDate>Sun, 01 Nov 2026 11:45:49 +0000</pubDate>
      <link>https://dev.to/jeff_li_deee6ec68274e6300/measuring-faces-in-the-browser-with-mediapipe-three-things-that-broke-3mk</link>
      <guid>https://dev.to/jeff_li_deee6ec68274e6300/measuring-faces-in-the-browser-with-mediapipe-three-things-that-broke-3mk</guid>
      <description>&lt;p&gt;I wanted a face-measurement tool with three properties most "face rating" sites don't have: the photo never leaves the browser, the same photo always gives the same numbers, and every formula is public. The result is &lt;a href="https://facialratio.com/" rel="noopener noreferrer"&gt;FacialRatio&lt;/a&gt;, and its engine is &lt;a href="https://github.com/newjia/facialratio-engine" rel="noopener noreferrer"&gt;open source&lt;/a&gt;. This post is about the parts that did not work the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Landmarks:&lt;/strong&gt; Google's MediaPipe Face Landmarker, loaded as WebAssembly only after the user picks a photo, so it never slows the first page load. It returns up to 478 points with a rough depth value for each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine:&lt;/strong&gt; one pure TypeScript function, &lt;code&gt;analyze(input)&lt;/code&gt;. No DOM, no randomness, no network. It takes landmark coordinates, not pixels, which makes it easy to unit-test against MediaPipe's canonical face mesh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; static files. There is no server endpoint that could receive an image, which is a simpler privacy promise than "we delete it after processing".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every length is divided by the pupil distance, so photo resolution doesn't matter. From there the engine computes 58 measurements and four scored parts: symmetry, facial thirds, facial fifths and a few golden-ratio comparisons. Each part is &lt;code&gt;100 − k × deviation&lt;/code&gt;, clamped to 0–100, and the overall score is a weighted sum (35 / 25 / 25 / 15). All the constants live in one file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: level heads looked tilted
&lt;/h2&gt;

&lt;p&gt;I started with a pitch gate of ±8°: tilt the head more than that and the tool asks for a retake. On 13 ordinary front-facing portraits, 7 were rejected. When I looked at the numbers, MediaPipe read heads that were visibly level at a &lt;strong&gt;median pitch of +8.2°&lt;/strong&gt;. One plainly level photo came out at 14.6°.&lt;/p&gt;

&lt;p&gt;The fix was boring: centre the window on the offset the model actually has. The gate is now −8° to +16°. The lesson is that a landmark model's pose output has its own bias, and you only see it if you test on everyday photos rather than the canonical mesh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: a slight head turn read as asymmetry
&lt;/h2&gt;

&lt;p&gt;Symmetry is measured by mirroring each landmark across a fitted midline and averaging the distance to its partner. On a perfectly frontal mesh that works. On a real photo with the head turned 5–8°, the far side of the face is foreshortened, and the engine reported it as asymmetry. That's exactly the kind of result that feels unfair to a user.&lt;/p&gt;

&lt;p&gt;Since MediaPipe gives a depth estimate per point, the engine now estimates yaw, rotates the points back to frontal, and only then measures. On the same 13 portraits, the &lt;strong&gt;median mirror error dropped from 7.31% to 2.28% of pupil distance&lt;/strong&gt;. Turns beyond ±10° still trigger a retake instead of being corrected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: the model can't see the hairline
&lt;/h2&gt;

&lt;p&gt;Facial thirds are usually measured hairline → brow → nose base → chin. MediaPipe has no hairline point. The highest point it offers sits on the upper forehead. So the upper third and the face length are estimates, and the tool says so in the result and in the report instead of pretending. The reference I use (18 / 41 / 41%) is defined from that same upper-forehead point, so at least the comparison is like-for-like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the golden ratio only gets 15%
&lt;/h2&gt;

&lt;p&gt;It's the most-searched part of the topic and the weakest evidence. Holland (2008) found that φ-based masks don't describe a wide range of faces, and Pallett, Link and Lee (2010) found preferred proportions that differ from φ. The tool still shows each ratio and its deviation from 1.618, because people want to see them, but it carries the lowest weight. Nothing in the tool is validated against human ratings, and the methodology page says that plainly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting without analytics code
&lt;/h2&gt;

&lt;p&gt;I wanted to know how many people finish an analysis without adding an analytics SDK. When an analysis completes, the page requests an empty static file such as &lt;code&gt;/e/done-golden-ratio-face.txt&lt;/code&gt;. The CDN's request log counts those requests. The request carries no image, score, cookie or identifier, only which step happened on which page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Collect a small set of everyday test photos on day one. The canonical mesh hides every real-world bias.&lt;/li&gt;
&lt;li&gt;Write down every constant in one config file from the start. It made the methodology page a direct render of the code, so the two can't drift apart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine, its tests and the canonical-face fixture are on &lt;a href="https://github.com/newjia/facialratio-engine" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. The full method, including known limits, is on the &lt;a href="https://facialratio.com/methodology" rel="noopener noreferrer"&gt;FacialRatio methodology page&lt;/a&gt;. Feedback on the measurement approach is very welcome.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>mediapipe</category>
    </item>
  </channel>
</rss>
