7678d28717
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.
66 lines
2.2 KiB
Bash
66 lines
2.2 KiB
Bash
#!/usr/bin/env bash
|
|
# Step 1 of the OCR pipeline: mild smoothing + contrast-stretch a receipt
|
|
# photo. Writes each intermediate stage (01_gray, 02_bilateral, 03_final)
|
|
# into the output directory so every step can be inspected independently
|
|
# before trusting the final image to OCR.
|
|
#
|
|
# Usage: ./1_preprocess_image.sh -in <photo> -out <scratch-dir>
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
|
ROOT="$(pwd)"
|
|
|
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
|
|
IN_PATH=""
|
|
OUT_DIR=""
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-in) IN_PATH="${2:-}"; shift 2 ;;
|
|
-out) OUT_DIR="${2:-}"; shift 2 ;;
|
|
*) fail "unknown argument: $1" ;;
|
|
esac
|
|
done
|
|
[ -n "$IN_PATH" ] && [ -n "$OUT_DIR" ] \
|
|
|| fail "usage: $0 -in <photo> -out <scratch-dir>"
|
|
[ -f "$IN_PATH" ] || fail "input file not found: $IN_PATH"
|
|
|
|
command -v docker >/dev/null 2>&1 || fail "docker not found in PATH"
|
|
|
|
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"
|
|
docker image inspect "$IMAGE" >/dev/null 2>&1 \
|
|
|| fail "$IMAGE not found locally - run ./build-image.sh first"
|
|
|
|
IN_ABS="$(realpath "$IN_PATH")"
|
|
IN_DIR="$(dirname "$IN_ABS")"
|
|
IN_FILE="$(basename "$IN_ABS")"
|
|
mkdir -p "$OUT_DIR"
|
|
OUT_ABS="$(realpath "$OUT_DIR")"
|
|
|
|
# On a vboxsf-mounted repo, files are owned by root:vboxsf with no
|
|
# "other" access - join the mounted dirs' groups so --user can read/write
|
|
# them without needing root inside the container.
|
|
GROUP_ADD_ARGS=(--group-add "$(stat -c %g "$IN_DIR")")
|
|
OUT_GID="$(stat -c %g "$OUT_ABS")"
|
|
[ "$OUT_GID" = "$(stat -c %g "$IN_DIR")" ] || GROUP_ADD_ARGS+=(--group-add "$OUT_GID")
|
|
|
|
echo "== Preprocessing $IN_FILE =="
|
|
docker run --rm \
|
|
--user "$(id -u):$(id -g)" \
|
|
"${GROUP_ADD_ARGS[@]}" \
|
|
-v "$IN_DIR:/data/in:ro" \
|
|
-v "$OUT_ABS:/data/out" \
|
|
"$IMAGE" \
|
|
python3 /opt/app/src/preprocess.py -in "/data/in/$IN_FILE" -out /data/out
|
|
|
|
echo "== Done =="
|
|
echo "Stages written to: $OUT_ABS/{01_gray,02_bilateral,03_final}.png"
|
|
echo "Next: ./2_ocr_image.sh -in $OUT_ABS/03_final.png -out $OUT_ABS"
|