Add adaptive preprocessing escalation and single-receipt test scripts
build / build (push) Successful in 2m57s
build / build (push) Successful in 2m57s
Preprocessing now tries progressively heavier variants per receipt (plain -> high-pass -> edge-enhance -> both), stopping as soon as a cheap LLM-free arithmetic check on the OCR text looks plausible, so the extra processing only applies to receipts that need it. A flat version of the same heavier processing regressed the full batch by helping hard receipts but hurting easy ones; the adaptive version improves date-matching 37/52 -> 40/52 with no regression (see testing/2026_10_04_gimp_highpass_nlfilter_experiment.md). Also adds test_ocr.sh and test_single.sh for checking a single receipt's OCR text or full pipeline result without a batch run.
This commit is contained in:
@@ -11,12 +11,17 @@ containers; there is no host Python environment to set up.
|
||||
## Pipeline
|
||||
|
||||
Three verifiable, independently-inspectable steps, each a thin host
|
||||
script that runs the actual work inside the `fuelish/tools` container:
|
||||
script that runs the actual work inside the `fuelish/tools` container
|
||||
(`server.py` runs the same functions in-process for the live app - see
|
||||
`12_adaptive_preprocess_ocr.sh` below for why steps 1+2 are combined
|
||||
there):
|
||||
|
||||
1. `1_preprocess_image.sh -in <photo> -out <dir>` - grayscale conversion,
|
||||
a mild bilateral filter, and a percentile contrast stretch. Writes
|
||||
every intermediate stage (`01_gray.png`, `02_bilateral.png`,
|
||||
`03_final.png`) so each step can be checked on its own.
|
||||
a mild bilateral filter, and a percentile contrast stretch (variant 0
|
||||
of `tooling-container/src/preprocess.py`'s 4 variants - see
|
||||
`12_adaptive_preprocess_ocr.sh`). Writes every intermediate stage
|
||||
(`01_gray.png`, `02_bilateral.png`, `03_final.png`) so each step can
|
||||
be checked on its own.
|
||||
2. `2_ocr_image.sh -in <dir>/03_final.png -out <dir>` - Tesseract OCR
|
||||
(German), writes `ocr.txt`.
|
||||
3. `3_extract_data.sh -in <dir>/ocr.txt -out <dir>` - sends the raw OCR
|
||||
@@ -24,13 +29,29 @@ script that runs the actual work inside the `fuelish/tools` container:
|
||||
schema, writes `extracted.json`.
|
||||
|
||||
Preprocessing choice is evidence-based, not assumed: heavier processing
|
||||
(strong denoising, CLAHE, unsharp masking) was tried and measured worse -
|
||||
it amplifies the receipt's dotted security-background pattern into OCR
|
||||
noise. The lighter bilateral+autocontrast approach in
|
||||
`tooling-container/src/preprocess.py` is what survived testing.
|
||||
(strong denoising, CLAHE, naive unsharp masking, and a GIMP-derived
|
||||
high-pass/edge-enhance combo - see
|
||||
`testing/2026_10_04_gimp_highpass_nlfilter_experiment.md`) consistently
|
||||
measured worse on the full batch *when applied unconditionally to every
|
||||
receipt* - each one amplifies the receipt's dotted security-background
|
||||
pattern into OCR noise, even though each visibly helps the specific hard
|
||||
receipts it was designed for. That experiment's accepted fix - trying
|
||||
heavier variants only on receipts the cheap pass already reads
|
||||
implausibly - is `12_adaptive_preprocess_ocr.sh` below.
|
||||
|
||||
## Supporting scripts
|
||||
|
||||
- `12_adaptive_preprocess_ocr.sh -in <photo> -out <dir>` - combines steps
|
||||
1+2 adaptively: tries `preprocess.py`'s 4 variants (0=baseline/same as
|
||||
`1_preprocess_image.sh`'s default, up to 3=highpass+edge-enhance
|
||||
combined) cheapest-first, scoring each variant's OCR text with
|
||||
`tooling-container/src/plausibility.py`'s cheap regex-based arithmetic
|
||||
check (no LLM call) and stopping at the first variant that looks
|
||||
receipt-consistent. This is what `test_batch.sh`, `test_single.sh`, and
|
||||
`server.py` actually use (not steps 1+2 directly) - see
|
||||
`testing/2026_10_04_gimp_highpass_nlfilter_experiment.md` for why.
|
||||
Writes `<dir>/variant<N>/` per variant tried plus `<dir>/ocr.txt` for
|
||||
the winning one.
|
||||
- `0_run_ollama.sh [-model <name>]` - starts Ollama as its own long-lived
|
||||
Docker container (`fuelish-ollama`, model weights in the named
|
||||
volume `fuelish-ollama-data`), pulling the model if not already
|
||||
@@ -50,22 +71,36 @@ noise. The lighter bilateral+autocontrast approach in
|
||||
creates sibling `<path>/<name>-scratch/` (intermediate step output per
|
||||
receipt) and `<path>/<name>-result/` (final `extracted.json` per
|
||||
receipt, plus `summary.txt`). Both are gitignored.
|
||||
- `test_single.sh -in <photo.jpg>` - the same pipeline run and
|
||||
ground-truth comparison as `test_batch.sh`, for one receipt instead of
|
||||
a whole directory - useful for re-checking a single known failure
|
||||
without re-running a full batch. Ground truth is optional: if a `*.ods`
|
||||
is found next to the photo it compares against it, otherwise it just
|
||||
prints the extracted JSON. Same sibling `-scratch`/`-result` layout as
|
||||
`test_batch.sh`, scoped to the one receipt.
|
||||
- `test_ocr.sh -in <photo.jpg>` - preprocessing + OCR only (no LLM
|
||||
extraction, no ground truth), printing the raw OCR text to stdout.
|
||||
Leaves no files behind (runs in a temp dir, cleaned up on exit) -
|
||||
for quickly checking what Tesseract actually reads off a receipt, e.g.
|
||||
while iterating on `tooling-container/src/preprocess.py`.
|
||||
|
||||
## Current state
|
||||
|
||||
Last full run against the 17-receipt ground-truth test set
|
||||
(`testdata/2026_new/`, not checked in - see `.gitignore`):
|
||||
Last full run against the 52-receipt ground-truth test set
|
||||
(`testdata/2026_benzin_kosten/`, not checked in - see `.gitignore`),
|
||||
with the adaptive preprocessing escalation described above:
|
||||
|
||||
- 15/17 receipts date-matched to ground truth (up from 11/17 with the
|
||||
original plain-grayscale preprocessing)
|
||||
- price_per_litre: 11/15 correct, litres: 9/15, discount: 11/15,
|
||||
total_price: 12/15
|
||||
- 40/52 receipts date-matched to ground truth (up from 37/52 with the
|
||||
single-variant/baseline-only preprocessing)
|
||||
- price_per_litre: 34/40 correct, litres: 24/40, discount: 34/40,
|
||||
total_price: 24/40
|
||||
|
||||
Known remaining failure modes, not expected to be fixable by further
|
||||
preprocessing tuning:
|
||||
Known remaining failure modes:
|
||||
|
||||
- OCR garbling on the compact "litres EUR/l price" line on some receipts
|
||||
(the raw text sometimes lacks recoverable digits)
|
||||
(the raw text sometimes lacks recoverable digits) even after trying
|
||||
all 4 preprocessing variants - the adaptive escalation above
|
||||
meaningfully improved this but didn't eliminate it
|
||||
- Occasional year-digit misreads in the date causing a false date-match
|
||||
failure
|
||||
- Low-resolution source photos (well below the ~4000x3000 the good
|
||||
@@ -79,9 +114,11 @@ text - a subtle failure mode worth remembering if the prompt is revised.
|
||||
|
||||
## Not yet built
|
||||
|
||||
Per the original idea (see git history / project notes): an Android app
|
||||
to capture and confirm receipts, a backend server to store raw + confirmed
|
||||
submissions, a simple authenticated web UI to query/filter/export
|
||||
(CSV/JSON), and cross-checking total_price against
|
||||
price_per_litre * litres - discount. None of that exists yet - this repo
|
||||
currently covers only the extraction pipeline.
|
||||
The Android app (`android/`), the HTTP job API (`docker-compose.yml`/
|
||||
`server.py`), and cross-checking total_price against
|
||||
price_per_litre * litres - discount (`android/.../ReceiptPlausibility.kt`
|
||||
on the app side, `tooling-container/src/plausibility.py` on the pipeline
|
||||
side) all now exist - see `README.md` for how they fit together. Not yet
|
||||
built: a web UI to query/filter/export outside the app, and auth/TLS on
|
||||
the server (deliberately out of scope for now - see `server.py`'s
|
||||
docstring).
|
||||
|
||||
Reference in New Issue
Block a user