<?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: Abhishek jha</title>
    <description>The latest articles on DEV Community by Abhishek jha (@abhipandit1).</description>
    <link>https://dev.to/abhipandit1</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%2F4130919%2Fc8d4cfa8-f53a-4483-bc49-cb6ee3f66db4.jpg</url>
      <title>DEV Community: Abhishek jha</title>
      <link>https://dev.to/abhipandit1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abhipandit1"/>
    <language>en</language>
    <item>
      <title>Segmentation is the hidden tax in historical HTR</title>
      <dc:creator>Abhishek jha</dc:creator>
      <pubDate>Fri, 18 Sep 2026 06:11:39 +0000</pubDate>
      <link>https://dev.to/abhipandit1/segmentation-is-the-hidden-tax-in-historical-htr-1i67</link>
      <guid>https://dev.to/abhipandit1/segmentation-is-the-hidden-tax-in-historical-htr-1i67</guid>
      <description>&lt;p&gt;When people talk about handwritten text recognition (HTR), they talk about the recogniser: the model that turns a strip of handwriting into text. That is the part with the leaderboard, the character error rate, the shiny numbers.&lt;/p&gt;

&lt;p&gt;But if you actually try to read a real historical page, the first thing that breaks is almost never the recogniser. It is the step nobody benchmarks: &lt;strong&gt;line segmentation&lt;/strong&gt;, cutting the page into the individual text lines the recogniser expects.&lt;/p&gt;

&lt;p&gt;Get that wrong and it does not matter how good your recogniser is. A line detector that merges two rows, or slices one row in half, hands the recogniser an input it was never trained on. The output comes back fluent and confidently wrong. I started calling this the hidden tax: a cost you pay before recognition even begins, and one that does not show up in recogniser benchmarks at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is worse on historical documents
&lt;/h2&gt;

&lt;p&gt;Modern OCR mostly assumes clean, printed, rectangular lines. Historical pages break every one of those assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Skew, curvature, and warped paper.&lt;/li&gt;
&lt;li&gt;Marginalia, insertions, and crossed-out text.&lt;/li&gt;
&lt;li&gt;Bleed-through from the reverse side.&lt;/li&gt;
&lt;li&gt;Dense tabular registers where a "line" is really a cell in a grid.&lt;/li&gt;
&lt;li&gt;Wildly varying hands, ink, and contrast across a single volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A generic line detector trained on modern documents degrades quietly here. It still returns boxes. They are just the wrong boxes, and you only notice when the transcription is garbage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I trained a domain-adapted &lt;strong&gt;YOLOv8&lt;/strong&gt; text-line detector on historical layouts (18th to 20th century Nordic court and church records), plus a &lt;strong&gt;kraken&lt;/strong&gt; baseline segmenter as a complementary, baseline-based approach. Box-based detection is fast and robust to the noise above; the baseline approach helps where reading order matters more than speed.&lt;/p&gt;

&lt;p&gt;Here is the box detector running on a real page it has never seen: an 1822 Nordic church register (public domain, via Wikimedia Commons). It finds 120 text lines, cell by cell, top to bottom:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fhuggingface.co%2Fabhishekjha1008%2Fyolov8m-historical-line-segmenter%2Fresolve%2Fmain%2Fassets%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fhuggingface.co%2Fabhishekjha1008%2Fyolov8m-historical-line-segmenter%2Fresolve%2Fmain%2Fassets%2Fdemo.gif" alt="Line segmentation on a real 1822 church register" width="560" height="898"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The detector reaches roughly &lt;strong&gt;mAP50 ~0.79&lt;/strong&gt; on held-out historical pages. That is not a solved-problem number, and I want to be honest about that: dense tables, heavy marginalia, and extreme skew still trip it up. But it is a large step up from a generic detector on this kind of material, and crucially it fails in visible ways (a missed or split box you can see) rather than the invisible failure of a good recogniser fed a bad crop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that surprised me
&lt;/h2&gt;

&lt;p&gt;While benchmarking a wider pipeline on unseen historical hands, the most striking failure mode was not the segmenter. It was the recognisers, specifically the general vision-language models.&lt;/p&gt;

&lt;p&gt;On truly unseen hands, a strong general VLM does not just score lower than a trained specialist. It &lt;strong&gt;fails silently&lt;/strong&gt;: it produces fluent, plausible text that is simply wrong, with no signal that anything went wrong. It will invent a year, a place, a name. A trained specialist, paired with good segmentation and a period-aware lexicon, stays far more faithful to what is actually on the page, and when it is unsure, it looks unsure.&lt;/p&gt;

&lt;p&gt;That combination, honest segmentation plus a faithful specialist, is the whole game for reading material that no model has seen before. (More on the full benchmark in a paper that is in preparation.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Everything here is open and runnable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Line segmenter (runnable):&lt;/strong&gt; &lt;a href="https://github.com/AbhiPandit1/danish-htr-line-segmenter" rel="noopener noreferrer"&gt;danish-htr-line-segmenter&lt;/a&gt; - a thin wrapper that downloads the weights from the Hub and segments a page in a few lines of Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model weights:&lt;/strong&gt; &lt;a href="https://huggingface.co/abhishekjha1008/yolov8m-historical-line-segmenter" rel="noopener noreferrer"&gt;yolov8m-historical-line-segmenter&lt;/a&gt; on the Hugging Face Hub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synthetic training data:&lt;/strong&gt; &lt;a href="https://huggingface.co/datasets/abhishekjha1008/danish-htr-synthetic" rel="noopener noreferrer"&gt;danish-htr-synthetic&lt;/a&gt; - 160,000 synthetic 18th-century Danish lines with exact ground truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; the &lt;a href="https://huggingface.co/spaces/abhishekjha1008/historical-danish-htr" rel="noopener noreferrer"&gt;Space&lt;/a&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;huggingface_hub&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HfApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hf_hub_download&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;ultralytics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;

&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abhishekjha1008/yolov8m-historical-line-segmenter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hf_hub_download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nc"&gt;HfApi&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;list_repo_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.pt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;YOLO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# boxes for each detected text line
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you work with historical documents, I would genuinely like to know what breaks on your material. The pages I cannot segment well are the most useful thing you can send me.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I work on historical-document HTR and production ML. Models and data: &lt;a href="https://huggingface.co/abhishekjha1008" rel="noopener noreferrer"&gt;hf.co/abhishekjha1008&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>ocr</category>
      <category>python</category>
      <category>digitalhumanities</category>
    </item>
  </channel>
</rss>
