Case Study 04 — Agentic Image Enhancer to train an in-house invoice reader | SatyaHQ
CASE 04 Document AI · Vision preprocessing

An agentic image enhancer — the unlock for an in-house invoice reader.

A third of incoming invoices arrived unreadable — phone photos with keystone distortion, faded thermal prints, scanner shadows, out-of-order attachments. No model was going to fix that. So we built a vision agent that inspects each page, picks a bespoke enhancement recipe, and only then hands off to labelling and training. Everything downstream got faster the moment the inputs got cleaner.

Unreadable → recovered
88%
Previously OCR-rejected pages, now processable
Accuracy from preprocessing alone
+23pts
Same extractor · dirty vs clean inputs
Enhancement recipes
4
Phone · thermal · scanner · multi-page
Median enhancement time
180ms
Per page · commodity CPU · no GPU needed
The context
A finance operations team building an in-house invoice reader after a commercial OCR product silently bypassed a third of the invoice volume.
Focus
Vision preprocessing · Enhancement agent
Approach
Per-pathology recipes · Adaptive · Escalation on quality-score threshold
Timeline
4-week build · Standalone component in a larger training pipeline
The premise

The model wasn't the bottleneck. The inputs were.

Every attempt to train an in-house invoice reader hit the same wall — dirty inputs meant slow, expensive labelling; slow labelling meant slow iteration; slow iteration meant a model that never surpassed the commercial OCR it was supposed to replace.

The enhancer agent broke that loop from the top. It takes a raw page, inspects it, decides which pathology dominates, and runs a bespoke recipe — perspective correction for phone photos, CLAHE for faded thermal prints, morphological background subtraction for scanner artefacts, page-splitting and reordering for multi-page attachments.

Below a legibility threshold, it doesn't guess — it escalates back to the sender for a re-capture. That refusal is what closes the loop on inbound quality over time.

4 recipesAdaptivePer-page quality scoreSender re-capture loopCPU-only

!
The single sentence that reframed the project

A mediocre extractor on clean images beats a good extractor on dirty ones — every time. Invest in the inputs before you invest in the model.

Where the enhancer sits

The keystone at the top of the training funnel.

The enhancer is the first agent every page meets. Everything downstream — labelling throughput, verifier accuracy, weekly fine-tune quality — inherits whatever it produces.

Enhancer → labels → training loop

upstream leverage · one component, four downstream effects
RAW INBOUND Phone photo Thermal print Scanner drop Multi-page email Native PDF STAGE 1 Enhancer Agent inspect → pick recipe → score EFFECT · LABELLING Human time: ~8 min → < 90 sec EFFECT · EXTRACTION First-pass field accuracy: +23 pts EFFECT · TRAINING Weekly fine-tune usable, not blocked EFFECT · CORPUS Re-capture loop lifts inbound quality DOWNSTREAM In-house Invoice Reader re-capture ask · corpus quality improves over time PATTERN Fix the inputs before you fix the model. Every 1% of pages the enhancer recovers → downstream throughput jumps by ~3%.
The four pathologies

What actually arrives in the queue.

A month of intake was classified by the failure mode that dominated each page. Four pathologies covered 96% of the previously-unreadable pile.

Pathology · 01

Phone photograph

Held at an angle, half-shadow, keystone distortion pulls the page into a trapezoid. Text lines curve across the frame.

Pathology · 02

Faded thermal print

The receipt has sat in a glovebox for months. The ink has evaporated into a narrow, low-contrast band that no threshold can separate.

Pathology · 03

Scanner artefacts

Binding-edge shadow, dust specks, and streaks. A global threshold either loses the text near the binding or turns the whole page into noise.

p.2 p.3 p.1
Pathology · 04

Multi-page attachment

Three pages of one invoice, mis-scanned upside-down, out of order, with a stray cover sheet. Line-item continuity is lost across the joins.

Shares total 96%. The remaining 4% — the genuinely destroyed — are the only pages that still route to a human on arrival.

Inside the agent

Inspect first. Choose a recipe. Score the output.

The enhancer isn't a fixed pipeline of filters. It runs a per-page decision — read the page, classify the dominant pathology, apply that pathology's recipe, and then score the result. If the score doesn't clear the bar, it doesn't fake it.

Enhancer agent — decision tree

Inspect → classify → recipe → score
RAW PAGE INSPECT CLASSIFY APPLY RECIPE SCORE OUTPUT Raw page any source, any DPI Inspect · dominant edges · histogram profile · page-count signal · skew estimate · shadow map Enhancer classify pathology → pick recipe RECIPE · PHONE 4-point perspective → adaptive threshold RECIPE · THERMAL CLAHE (wide tile) → gentle sharpen RECIPE · SCANNER Morphological BG sub. → binarise clean field RECIPE · MULTI-PAGE Split · orient · reorder → per-page pipeline SCORE ≥ threshold? Clean page + score → handoff to layout Re-capture ask automated reply to sender pass fail per-recipe pass rates → classifier's routing prior GUARANTEE Never guess on a low-quality page. Refuse and ask.
Recipe deep-dive

Four pathologies, four bespoke pipelines.

Each recipe is a tight sequence of CV operations tuned to one specific failure. Below, the transform that each recipe performs — with the input state on the left and the recovered state on the right.

1
Recipe · Phone

4-point perspective correction

INPUT 4 corners detected homography H warpPerspective OUTPUT flat · orthogonal · ready to binarise

The agent finds the four page corners (contour detection, then Hough-line refinement) and derives the homography that maps the distorted quadrilateral to a rectangle. Only then does adaptive thresholding kick in — on a page that finally has straight text lines.

detectPageCorners() getPerspectiveTransform() warpPerspective() adaptiveThreshold()
2
Recipe · Thermal

CLAHE contrast lift

INPUT HISTOGRAM 0 255 narrow band · low separation CLAHE tile 24×24, clip 3.0 OUTPUT HISTOGRAM 0 255 full dynamic range · text separates cleanly

A global histogram stretch smears the noise as much as it lifts the ink. CLAHE (Contrast-Limited Adaptive Histogram Equalisation) equalises in tiles and caps the amplification per tile — the ink separates from the paper without blowing up the speckle.

cvtColor(GRAY) CLAHE(clipLimit=3, tile=24) unsharpMask(σ=1.2)
3
Recipe · Scanner

Morphological background subtraction

INPUT shadow + specks estimate BG · subtract morphologyEx OPEN 40px OUTPUT shadow removed · specks gone

A large morphological OPEN estimates the smooth "paper" background (which contains the shadow but not the text). Subtract that from the page and the shadow disappears — leaving a uniformly-lit surface that a global threshold handles cleanly. Speckle removal follows with a small median.

morphologyEx(OPEN, kernel=40) subtract(bg, page) medianBlur(3) threshold(OTSU)
4
Recipe · Multi-page

Split · orient · reorder

INPUT p.2 p.3 p.1 2 → 3 → 1 · one flipped segment · detect · sort Tesseract OSD + page-N regex OUTPUT p.1 p.2 p.3 upright · sequential · one invoice

The file is segmented into individual pages; each page runs orientation-detection (Tesseract OSD) and rotates upright; page-number strings ("p. 2 of 3") are extracted and used to sort. The reader downstream receives one coherent, ordered document instead of a shuffled deck.

pdfToImages() osd.detect_orientation() rotate(k*90) regex(/p(?:age)?\.?\s*(\d+)/) sort()
The quality gate

Below the threshold, the agent refuses to guess.

Every enhanced page carries a legibility score. Below the bar, the agent doesn't send half-recovered text downstream — it replies to the sender asking for a re-capture, and the corpus gets cleaner over time.

Distribution of legibility scores

Steady-state month 3 · scored on enhanced output
30%20%10%0% 0.0 0.2 0.4 0.6 0.8 1.0 threshold 0.55 FAIL · re-capture · 5.1% PASS · downstream · 94.9%
Passes threshold
Below threshold — re-capture ask

Recovery from the previously-unreadable pile

Pages the commercial OCR had rejected · now processable
88% recovered
Recovered by enhancer 88%
Genuinely destroyed 12%

Every recovered page is now a training example — the enhancer directly grew the corpus.

Downstream impact

The extractor didn't get smarter. The pages got cleaner.

We froze the extractor model and ran it against the same document mix, once with the enhancer in front and once without. Every metric that mattered moved.

Same extractor · dirty vs enhanced inputs

First-pass field accuracy across the messy pile
100%75%50%25% 38% 89% Phone 46% 92% Thermal 55% 88% Scanner 41% 87% Multi-page Dirty inputs Enhancer applied

The gap is bigger on the pathology the enhancer targets.

On the phone-photograph subset — where perspective distortion made the extractor nearly useless — accuracy jumped 51 points. On scanner artefacts, where the extractor was already limping along, the lift was 33 points.

Across the whole messy pile the aggregate lift settled at +23 points of first-pass field accuracy, from the same extractor weights.

Compounding effect on training

Clean inputs meant cheap labels. Cheap labels meant faster iteration. Faster iteration meant the in-house reader caught up to — and then surpassed — the commercial OCR inside a quarter.

Enhancer coverage over the build

Share of intake handled by an active recipe · weekly · first 14 weeks
Coverage %
100%75%50%25% W1W2W3W4 W5W6W7W8 W9W10W11 W12W13W14 RECIPE 1 · Phone RECIPE 2 · Thermal RECIPE 3 · Scanner RECIPE 4 · Multi-page 96% · four recipes cover the intake
Before vs after

The workflow, side-by-side.

Before — Preprocessing off After — Enhancer agent in front
~⅓ of pages effectively bypassed the OCR — the commercial product refused them. Enhancer recovers 88% of that previously-unreadable pile.
First-pass field accuracy on the messy mix: ~72%. Same extractor, enhanced inputs: 95% (+23 pts).
Labelling time: ~8 minutes per invoice (humans starting from raw). Labelling on enhanced pages: under 90 seconds per invoice.
Fine-tune cadence: quarterly, blocked on labelled data. Weekly fine-tune loop, no longer starved for training examples.
Bad pages silently made it into the ERP — verified downstream. Sub-threshold pages refuse to proceed — automated re-capture ask instead.
The impact

One agent. Four recipes. The reader that couldn't be trained, trained.

Accuracy lift
+23pts
Same extractor · enhancer only
Corpus growth
+34%
Trainable pages, month over month
Coverage
96%
Intake handled by an active recipe
Runtime
180ms
Median enhancement · CPU-only

"We spent six months trying to squeeze more accuracy out of the extractor. Then we spent four weeks fixing the inputs — and got more lift than the six months combined. The unlock was staring at us in the raw folder the whole time."

— Lead ML engineer, in-house reader team
What we learned

Four lessons from building the enhancer.

Lesson 01

The classifier is more valuable than the filters.

The individual CV operations are decades-old. What made the enhancer valuable was the agent that decided which operation to run — a single filter chain on everything would have blurred the wins.

Lesson 02

The refusal is a feature, not a fallback.

The re-capture ask isn't the enhancer failing — it's the enhancer preventing bad data from poisoning downstream training. It also nudged the inbound corpus to get cleaner over time.

Lesson 03

Ship one recipe at a time.

We shipped Recipe 1 (phone) in week 2 and measured its impact before starting Recipe 2. That kept us honest — no recipe went live until we'd proven the previous one moved the number it was supposed to.

Lesson 04

Version the recipes too.

Each recipe is a versioned artefact with its own gold-set. When a downstream regression appears, we can bisect not just the model but the enhancement pipeline that produced its training data.

What's next

A fifth recipe, and a portable pattern.

Two extensions are in flight. First: a recipe for handwritten annotations (initials, stamps, marginalia) that today confuse the extractor. Second: applying the same "inspect-then-recipe" pattern to non-invoice documents where dirty inputs are the bottleneck.

Where the pattern goes next

One agent shape · new document classes
Today +6 weeks +14 weeks LIVE Invoices 4 recipes · 96% coverage BUILD Recipe 5 — handwritten initials · stamps · marginalia PILOT POs · Receipts · KYC same shape, new classifier SHAPE · INSPECT PAGE → CLASSIFY PATHOLOGY → APPLY RECIPE → SCORE
Deliverables

What ships with the enhancer.

Enhancer service

  • Python service · CPU-only · sub-200ms median
  • 4 versioned recipes + pluggable classifier
  • Legibility scoring + configurable threshold
  • Automated re-capture reply to sender

Evaluation harness

  • Per-recipe gold-set (150 pages each)
  • Per-pathology accuracy dashboard
  • Regression bisect on recipe + extractor
  • Quality-score calibration tooling

Ops tooling

  • Coverage & refusal-rate dashboard
  • Corpus-quality trend (inbound over time)
  • Sender-side re-capture success metrics
  • Alerting on recipe-level pass-rate drops
PythonOpenCVTesseract OSDFastAPISub-200ms · CPUWeights & BiasesGrafana
Related case study

01 — Invoice ingestion, automated end-to-end

The AP pipeline this enhancer plugs into — 40K invoices/month across 200+ supplier formats.

Read case 01 →