feat(ocr): adaptive binarisation + per-word confidence filter

Two layers to stop tesseract hallucinating text on phone photos:

1. Bradley adaptive threshold (O(N) via integral images) turns the
   downscaled grayscale image into a binary B/W mask. Cuts out
   gradients, shadows, and surface texture that tesseract
   misreads as ink.
2. After recognise(), filter data.words by confidence >= 60 and
   re-assemble lines from kept words. Status line surfaces how
   many were dropped so the cost is visible.

Output of preprocess stage is PNG (not JPEG) so the sharp B/W
edges don't get re-blurred.
This commit is contained in:
João Pedro Battistella Nadas
2026-06-09 18:54:27 +02:00
parent dcac01e51b
commit 4d4e351860
2 changed files with 92 additions and 18 deletions

View File

@@ -10,6 +10,8 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Added
- "Scan image" button next to the input pane: opens the phone's rear camera (or a file picker on desktop), runs OCR locally in the browser via Tesseract.js (loaded from esm.sh on first use), and replaces the editor contents with the recognised text. Language follows the "From" dropdown via an ISO 639-2 mapping; "Auto-detect" loads a common European set (`eng+por+deu+nld`). Progress status shows beneath the input ("Loading OCR engine…" → "OCR 40%" → "OCR done in 3.2s"). Zero image-size cost — same CDN pattern as CodeMirror.
- Images are downscaled to 1600 px on the long edge before OCR. A 12 MP phone photo (~4000 px) drops to ~1.3 MP, making Tesseract 4-8× faster with no real accuracy loss for printed text. Smaller-than-1600 px inputs are passed through unchanged.
- Adaptive thresholding (Bradley's method with integral images) binarises the downscaled image before OCR — drops hallucinated text from shadows, gradients, and surface texture on phone photos. Output is pure black-on-white, much closer to the scanned-page input Tesseract was trained on.
- Per-word confidence filter (≥ 60) drops the low-confidence noise Tesseract still emits. Re-assembles lines from kept words; status line reports how many words were dropped.
### Fixed
- "Hot: …" status no longer claims "unloads in X" when the server's `keep_alive` is `-1` (Ollama returns an absurd year-4001 `expires_at`). Threshold: any expiry >7 days drops the suffix and just shows the model name.