<?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: Paul</title>
    <description>The latest articles on DEV Community by Paul (@snakelizzard).</description>
    <link>https://dev.to/snakelizzard</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3927064%2Ff1544cd0-8741-4e2e-89b2-808c336c2179.jpg</url>
      <title>DEV Community: Paul</title>
      <link>https://dev.to/snakelizzard</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/snakelizzard"/>
    <language>en</language>
    <item>
      <title>How I Built a Document Detector in the Browser</title>
      <dc:creator>Paul</dc:creator>
      <pubDate>Tue, 12 May 2026 11:49:04 +0000</pubDate>
      <link>https://dev.to/snakelizzard/how-i-built-a-document-detector-in-the-browser-2g0c</link>
      <guid>https://dev.to/snakelizzard/how-i-built-a-document-detector-in-the-browser-2g0c</guid>
      <description>&lt;p&gt;Scanning a document with your phone is one of those small tasks that comes up all the time. You need a decent photo of a page, and you need to send it somewhere quickly - by email, in a messenger, wherever. Usually that means reaching for an app. Modern browsers can run fairly serious code with WASM, so in some cases &lt;a href="https://phonescan.me" rel="noopener noreferrer"&gt;opening a site&lt;/a&gt; is easier than installing yet another app.&lt;/p&gt;

&lt;p&gt;It turned out to be a good computer vision problem. Real photos of documents are messy in all the usual ways: perspective distortion, poor lighting, glare, shadows, and cluttered backgrounds. Sometimes the page is partly out of frame. Sometimes the background itself is full of straight lines and rectangles that are easy to mistake for the document.&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.amazonaws.com%2Fuploads%2Farticles%2Fq3k8utp2dfgb1s5gvykz.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.amazonaws.com%2Fuploads%2Farticles%2Fq3k8utp2dfgb1s5gvykz.png" alt=" " width="300" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Overall Approach
&lt;/h2&gt;

&lt;p&gt;I wanted to keep the whole thing relatively simple and run everything on the client. So instead of going down the neural network route, I built the detector with classical computer vision methods.&lt;/p&gt;

&lt;p&gt;The core idea is simple: don't trust any single detection method.&lt;/p&gt;

&lt;p&gt;On real phone photos, that just isn't robust enough.&lt;/p&gt;

&lt;p&gt;Instead, I run the image through several different processing paths. Each one tries, in its own way, to separate the document from the rest of the scene. From those results, I collect regions that look like a sheet of paper, build candidates, and only then choose the best one.&lt;/p&gt;

&lt;p&gt;In practice, this worked much better than trying to find the document with one "main" method. Some approaches look good under clean, even lighting and then immediately fall apart on glare. Others survive shadows better but get confused by a busy background. What helped most was not finding one perfect method, but combining several imperfect ones that fail differently.&lt;/p&gt;

&lt;p&gt;My first pass relies on the visual traits that often make paper stand out from the background. Another tries to pull out borders through local contrast. A third focuses more directly on sharp transitions and lines. After each pass, I extract contours, and those contours become the raw material for the candidate selection stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Detector Chooses the Document Candidate
&lt;/h2&gt;

&lt;p&gt;Once I have a set of contours, the next problem is figuring out which ones really look like a document.&lt;/p&gt;

&lt;p&gt;This is where geometry starts to matter.&lt;/p&gt;

&lt;p&gt;For each contour, I try to recover a shape with four corners. Sometimes that works right away. Sometimes I need to simplify the contour a little first. And sometimes the contour is too messy, so I fall back to a rough rectangular estimate as a backup.&lt;/p&gt;

&lt;p&gt;From there, I check each candidate against a few fairly intuitive rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It should look like a convex quadrilateral;&lt;/li&gt;
&lt;li&gt;Its sides should have plausible proportions;&lt;/li&gt;
&lt;li&gt;Its angles and diagonals should not look too distorted;&lt;/li&gt;
&lt;li&gt;If it leans too heavily on the image borders, that's usually suspicious.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This does not mean the document has to look like a perfect rectangle. In mobile photos, perspective distortion is almost guaranteed. But even with perspective, you can usually tell the difference between a real sheet of paper and some random background object that only vaguely resembles one.&lt;/p&gt;

&lt;p&gt;This stage turned out to be one of the most useful ones for cutting down false positives. Many obvious troublemakers tend to drop out here: boxes, table edges, frames, screens, and other rectangular objects in the scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the pipeline
&lt;/h2&gt;

&lt;p&gt;Here's the short version of the pipeline.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Stage&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;What happens&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Algorithms and details&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Why it helps&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Preprocessing&lt;/td&gt;
&lt;td&gt;I generate several versions of the image where the document may stand out in different ways&lt;/td&gt;
&lt;td&gt;Paper mask based on HSV and Lab features, grayscale normalization via top-hat + Otsu, separate adaptive-threshold passes in normal and inverted form, plus Canny&lt;/td&gt;
&lt;td&gt;This gives me multiple hypotheses instead of forcing everything to depend on one fragile segmentation method&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mask cleanup&lt;/td&gt;
&lt;td&gt;I remove small noise, close gaps, and merge nearby regions&lt;/td&gt;
&lt;td&gt;Morphological operations: open / close / dilate&lt;/td&gt;
&lt;td&gt;This makes document regions more coherent and cuts down on random fragments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region search&lt;/td&gt;
&lt;td&gt;On each pass, I look for contours and turn them into raw candidates&lt;/td&gt;
&lt;td&gt;Contour detection on multiple masks and binarization variants&lt;/td&gt;
&lt;td&gt;Different passes find the document under different conditions, so this helps build a better candidate pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Candidate construction&lt;/td&gt;
&lt;td&gt;Each contour is converted into a quadrilateral, or at least something close to one&lt;/td&gt;
&lt;td&gt;approxPolyDP on the original contour with different epsilon values, then on the convex hull; fallback: minAreaRect&lt;/td&gt;
&lt;td&gt;This gives more stable candidates even when contours are noisy, jagged, or partly broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shape validation&lt;/td&gt;
&lt;td&gt;I keep only the shapes that still plausibly look like a document&lt;/td&gt;
&lt;td&gt;Corner ordering TL / TR / BR / BL, convexity checks, aspect ratio, consistency of opposite sides and diagonals, handling for frame-border contact&lt;/td&gt;
&lt;td&gt;This filters out a lot of rectangular background clutter and reduces false positives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overall scoring&lt;/td&gt;
&lt;td&gt;Each candidate gets a final score, and I pick the best one&lt;/td&gt;
&lt;td&gt;Score built from several metrics: angle rectangularity, photometric contrast inside vs. outside the polygon, closeness to center, preferred area, border-touch penalty, then normalization into confidence&lt;/td&gt;
&lt;td&gt;This makes the final decision less brittle and keeps the detector reasonably explainable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important part here is that no individual stage has to be perfect. The detector becomes much more reliable because these steps reinforce one another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Approach Worked Better Than I Expected
&lt;/h2&gt;

&lt;p&gt;One thing I liked about this pipeline is that it stayed fairly lightweight while still being flexible enough for messy real photos.&lt;/p&gt;

&lt;p&gt;I wasn't trying to make the detector overly clever. I mostly wanted something predictable, explainable, and fast enough to run entirely in the browser. That pushed me toward classical CV methods from the start. They are less fashionable than neural nets, but for this kind of problem they still give you a lot to work with.&lt;/p&gt;

&lt;p&gt;Another thing that helped was treating the final choice as a scoring problem instead of a hard yes-or-no decision at every step. A candidate does not need to be perfect in every way. It just needs to look better overall than the alternatives. In practice, that made the detector behave much more reasonably on borderline cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd Love a Second Opinion
&lt;/h2&gt;

&lt;p&gt;What I'm most interested in now is where this kind of pipeline still feels brittle: which parts look solid, which checks seem genuinely useful, and which ones feel questionable or overly heuristic.&lt;/p&gt;

&lt;p&gt;And, honestly, real photos are where all of this gets tested properly. Synthetic examples are neat, but actual phone shots tend to reveal weak spots much faster. That's usually where the most useful feedback comes from.&lt;/p&gt;

&lt;p&gt;So, do you want to &lt;a href="https://phonescan.me" rel="noopener noreferrer"&gt;give it a try&lt;/a&gt; with your samples?&lt;/p&gt;

</description>
      <category>computervision</category>
      <category>algorithms</category>
    </item>
  </channel>
</rss>
