Add containerized OCR pipeline for fuel receipts
Self-hosted, verifiable pipeline for extracting structured data (station, date/time, price per litre, litres, discount, total) from German fuel receipt photos: grayscale + mild bilateral filter + contrast stretch, Tesseract OCR, then LLM structuring via a small local model (Qwen 2.5, served by Ollama). Each step is independently runnable and inspectable through numbered scripts against a shared tooling container. Ollama runs as its own long-lived container (0_run_ollama.sh), separate from the stateless per-receipt pipeline steps, so the model stays loaded in memory between runs. Includes a containerized ground-truth batch test (test_batch.sh) that reproduces the full pipeline over the test receipts and reports per-field accuracy. Targets eventual deployment on a Raspberry Pi 4 k3s cluster.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test-only tool: runs the full container pipeline (1_preprocess_image.sh,
|
||||
# 2_ocr_image.sh, 3_extract_data.sh) over every *.jpg in a test-data
|
||||
# directory, then compares the results against that directory's
|
||||
# ground-truth spreadsheet (benzin_kosten.ods). Everything -
|
||||
# preprocessing, OCR, structuring, and the comparison itself - runs
|
||||
# inside the tooling container; nothing here depends on a host Python
|
||||
# environment.
|
||||
#
|
||||
# For an input directory <path>/<name>, two sibling directories are
|
||||
# created (mirroring <path>/<name>):
|
||||
# <path>/<name>-scratch/<receipt>/ - intermediate step output
|
||||
# (01_gray.png, 02_bilateral.png,
|
||||
# 03_final.png, ocr.txt) for
|
||||
# inspecting each step on its own
|
||||
# <path>/<name>-result/<receipt>/ - final extracted.json per receipt,
|
||||
# plus summary.txt with the overall
|
||||
# ground-truth comparison
|
||||
#
|
||||
# Usage: ./test_batch.sh -in <test-data-dir>
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
ROOT="$(pwd)"
|
||||
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
IN_DIR=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-in) IN_DIR="${2:-}"; shift 2 ;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$IN_DIR" ] || fail "usage: $0 -in <test-data-dir>"
|
||||
[ -d "$IN_DIR" ] || fail "input directory not found: $IN_DIR"
|
||||
|
||||
IN_ABS="$(realpath "$IN_DIR")"
|
||||
ODS_PATH="$(find "$IN_ABS" -maxdepth 1 -name '*.ods' | head -n1)"
|
||||
[ -n "$ODS_PATH" ] || fail "no .ods ground-truth file found in $IN_ABS"
|
||||
|
||||
PARENT_DIR="$(dirname "$IN_ABS")"
|
||||
BASE_NAME="$(basename "$IN_ABS")"
|
||||
SCRATCH_DIR="$PARENT_DIR/$BASE_NAME-scratch"
|
||||
RESULT_DIR="$PARENT_DIR/$BASE_NAME-result"
|
||||
mkdir -p "$SCRATCH_DIR" "$RESULT_DIR"
|
||||
|
||||
shopt -s nullglob
|
||||
IMAGES=("$IN_ABS"/*.jpg)
|
||||
[ "${#IMAGES[@]}" -gt 0 ] || fail "no .jpg files found in $IN_ABS"
|
||||
|
||||
echo "== Running pipeline over ${#IMAGES[@]} receipts =="
|
||||
for img in "${IMAGES[@]}"; do
|
||||
name="$(basename "${img%.jpg}")"
|
||||
receipt_scratch="$SCRATCH_DIR/$name"
|
||||
receipt_result="$RESULT_DIR/$name"
|
||||
mkdir -p "$receipt_scratch" "$receipt_result"
|
||||
echo "--- $name ---"
|
||||
./1_preprocess_image.sh -in "$img" -out "$receipt_scratch" >/dev/null
|
||||
./2_ocr_image.sh -in "$receipt_scratch/03_final.png" -out "$receipt_scratch" >/dev/null
|
||||
./3_extract_data.sh -in "$receipt_scratch/ocr.txt" -out "$receipt_result" >/dev/null
|
||||
done
|
||||
|
||||
echo
|
||||
echo "== Comparing against ground truth =="
|
||||
|
||||
IMAGE="${REGISTRY_IMAGE:-gastronomy/tools}"
|
||||
if [ -f "$ROOT/registry.env" ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$ROOT/registry.env"
|
||||
IMAGE="${REGISTRY_IMAGE:-$IMAGE}"
|
||||
fi
|
||||
VERSION="$(<"$ROOT/tooling-container/VERSION")"
|
||||
IMAGE="$IMAGE:$VERSION"
|
||||
|
||||
ODS_DIR="$(dirname "$ODS_PATH")"
|
||||
ODS_FILE="$(basename "$ODS_PATH")"
|
||||
GROUP_ADD_ARGS=(--group-add "$(stat -c %g "$ODS_DIR")")
|
||||
RESULT_GID="$(stat -c %g "$RESULT_DIR")"
|
||||
[ "$RESULT_GID" = "$(stat -c %g "$ODS_DIR")" ] || GROUP_ADD_ARGS+=(--group-add "$RESULT_GID")
|
||||
|
||||
docker run --rm \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
"${GROUP_ADD_ARGS[@]}" \
|
||||
-v "$ODS_DIR:/data/gt:ro" \
|
||||
-v "$RESULT_DIR:/data/results:ro" \
|
||||
"$IMAGE" \
|
||||
python3 /opt/app/src/compare_ground_truth.py -gt "/data/gt/$ODS_FILE" -results /data/results \
|
||||
| tee "$RESULT_DIR/summary.txt"
|
||||
|
||||
echo
|
||||
echo "== Done =="
|
||||
echo "Intermediate step output: $SCRATCH_DIR/<receipt>/"
|
||||
echo "Final results + summary: $RESULT_DIR/<receipt>/, $RESULT_DIR/summary.txt"
|
||||
Reference in New Issue
Block a user