Questions about XLiteOCR? The source is on GitHub; for anything else, reach out:
contact@edgexene.ioMost OCR engines return text and position, and throw away everything else about how the text looked. XLiteOCR returns the color of the text in every region as well, as hex, RGB, and a readable color name.
Three uses, in rough order of how often they come up.
An invoice with APPROVED stamped across it in red and a paragraph containing the word "approved" produce the same string from every engine that discards color. Recovering the color separates a mark made on the document from the text of the document, which is usually the distinction a workflow cares about.
Brand color is a fact about a page that is normally re-keyed by hand. Extracting it alongside the text and the vectorized figures keeps the whole page machine-readable in one pass.
Knowing the color of the ink and the color of the paper gives the distance between them. When that distance is near zero the text is still there, still selectable, still readable by a machine, and invisible to a person. White on white, or gray a shade off the paper. Sometimes that is a careless template. Sometimes it is a paragraph concealed in a document that is about to be handed to someone.
The obvious implementation is to crop the region, count pixels, and treat the majority color as the background. That works on body copy and fails on three ordinary cases.
The letterforms cover more than half the crop, so the majority color is the ink and the result comes back inverted: white text on a black page, reported confidently.
Sampling the edges of the crop instead does not help. Text detectors draw tight boxes, so those edges cut through the glyphs. The sample is ink, labeled as paper.
Every glyph is surrounded by pixels blended between ink and paper, and on thin type there can be more blended pixels than solid ones. Averaging the foreground returns a color that appears nowhere on the actual page.
XLiteOCR handles all three in about two hundred lines. Otsu's method splits the crop into a dark class and a light class by maximizing between-class variance, so the threshold follows the exposure of the image rather than a fixed guess. The darker class is treated as the ink, since ink on paper is the common case; that assumption is overturned only when the dark class covers more than 65% of the crop and is close to uniform in color, which describes a solid banner and does not describe heavy text. K-means with k=2 then runs over the stroke pixels, and the cluster returned is the one furthest from the background rather than the most populous one. That last choice is what steps past the anti-aliasing blend and lands on the color of the letter itself.
There is no OpenCV in this path, which also keeps the dependency stack clear of OpenCV's bundled-codec licensing question.
The color detection in XLiteOCR now also runs inside PATANYX, EdgeXene's privacy browser. PATANYX is written in Rust and ships as a single binary with no Python in it, which is usually where a Python component stops being portable.
It ported cleanly because the algorithm is arithmetic rather than a framework. NumPy was doing multiplication; it was not holding the design together. The Rust version is a direct translation onto an image library the browser already carried, and it added no new dependencies to a project that treats each one as a decision.
In the browser the measurement answers the third question above. When PATANYX checks a photo before it is shared, it finds text that is too faint to see and shows you what it says. The feature built to tell a red stamp from black body copy finds concealment as a consequence of measuring color.
Above roughly two-thirds coverage by a single flat color, heavy dark text on a light background and light text on a dark banner are genuinely indistinguishable in the pixels, and XLiteOCR resolves them as a banner. The 65% figure is a deliberate constant rather than a default.
A test written during the Rust port asserted that heavy red text on white should still report red at 75% ink coverage. It failed, and the algorithm was right: at that coverage with both colors flat, the image is white text on a red banner as far as any measurement can tell. The constant is 0.65 and not 0.50 for exactly this reason.
Per-region color reports the dominant color of a region, not per-character colors. Text that changes color mid-line returns the dominant one.