<?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: Maksym Yastremskyi</title>
    <description>The latest articles on DEV Community by Maksym Yastremskyi (@yasmax91).</description>
    <link>https://dev.to/yasmax91</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%2F4087985%2F984d2c2e-55e5-429c-8cbd-6bb53ea4db1f.png</url>
      <title>DEV Community: Maksym Yastremskyi</title>
      <link>https://dev.to/yasmax91</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yasmax91"/>
    <language>en</language>
    <item>
      <title>The bug was in the colour space, not the parser</title>
      <dc:creator>Maksym Yastremskyi</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:58:05 +0000</pubDate>
      <link>https://dev.to/yasmax91/the-bug-was-in-the-colour-space-not-the-parser-3hna</link>
      <guid>https://dev.to/yasmax91/the-bug-was-in-the-colour-space-not-the-parser-3hna</guid>
      <description>&lt;p&gt;A schedule arrives as an image. Each cell is colour-coded — one colour per shift type — so reading the schedule means reading the colours. The first version did the obvious thing: sample the pixel, compare the RGB triple against a table of known colours, take the nearest one.&lt;/p&gt;

&lt;p&gt;It worked on the files we tested with. It fell apart on the files users actually sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks
&lt;/h2&gt;

&lt;p&gt;Nobody sends the original export. They screenshot it, forward it through a messenger, and the messenger re-encodes it. JPEG quantisation shifts values, chroma subsampling smears the boundary between adjacent cells, and a second forward compounds both. A colour that left as &lt;code&gt;#F4B183&lt;/code&gt; can arrive as &lt;code&gt;#F2B57F&lt;/code&gt; — invisible to a human, three channels off for a table lookup.&lt;/p&gt;

&lt;p&gt;You can widen the tolerance, and that is where the trap closes: widen it enough to absorb the drift, and two of the palette's colours start claiming the same pixels. Narrow it enough to keep them apart, and half the real cells match nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why RGB is the wrong ruler
&lt;/h2&gt;

&lt;p&gt;RGB distance measures how far apart two colours are &lt;em&gt;as numbers&lt;/em&gt;, not how different they look. The same numeric distance is a barely visible shift in one part of the space and an obvious change in another. So "nearest colour in RGB" answers a question nobody asked.&lt;/p&gt;

&lt;p&gt;CIE-Lab was designed for the question we actually have. It is built so that Euclidean distance between two colours approximates how different they look to a human eye. Compression artefacts are tuned — deliberately — to stay below human perception, which means they also stay small in Lab. The distortion RGB reports as a large numeric jump is, in Lab, a small one.&lt;/p&gt;

&lt;p&gt;The change was mechanical: convert both the sample and the palette to Lab, classify by nearest neighbour there, keep a rejection threshold so an unknown colour stays unknown instead of being forced into the palette.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the cells in the first place
&lt;/h2&gt;

&lt;p&gt;Colour classification only matters once you know where a cell is. Grid detection here is morphological rather than model-based: erode along one axis with a long thin kernel to leave only horizontal runs, repeat along the other axis for vertical runs, intersect the two to get line candidates, then cluster the coordinates to collapse a three-pixel-thick line into one index.&lt;/p&gt;

&lt;p&gt;It is unglamorous and it has a property no learned detector gave us for free: when it fails, it fails visibly and locally — one missing line, not a plausible-looking grid shifted by a row.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;When a pipeline is flaky and the logic reads correctly, the bug is usually in a representation nobody is looking at: the colour space, the encoding, the timezone, the locale, the collation. The code was never wrong. It was measuring the right thing with the wrong ruler.&lt;/p&gt;

&lt;p&gt;That parser now runs against real user uploads on a Telegram bot, where every input has been through at least one round of recompression before it arrives.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://yastremskyi.com/notes/colour-space-ocr" rel="noopener noreferrer"&gt;yastremskyi.com/notes/colour-space-ocr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>computervision</category>
      <category>debugging</category>
      <category>ocr</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
