Files
canteen-asset-tracker/docs/OCR_PIPELINE.md
T
shawn 17b870e4cc feat: wire vision/OCR results back into asset records
- Auto-save photo_path to matched assets when photo uploaded via /api/ocr
- Auto-extract and save serial_number from OCR text to blank-serial matched assets
- Add _extract_serial_from_text helper for serial number pattern matching
- Create scripts/backfill_vision_photos.py for batch-processing unlinked photos
- Add docs/OCR_PIPELINE.md documenting the full OCR/vision pipeline
- Fix test DB schema: add connect_id, equipment_id, barcode, customer_name
- Fix OCR test assertions to match actual endpoint behavior
2026-05-29 10:03:51 -04:00

175 lines
7.3 KiB
Markdown

# OCR / Vision Pipeline
> End-to-end: camera capture → text extraction → asset matching → auto-update.
The Canteen Asset Tracker has a multi-stage OCR pipeline that reads machine ID sticker photos, cross-references extracted identifiers against the assets database, and automatically updates matched assets with photo paths and serial numbers.
## Pipeline Stages
```
Camera/Gallery Photo
┌──────────────────┐ Stage 1: Vision (Ollama qwen2.5vl:3b)
│ Ollama Vision │ Runs first — most accurate on complex labels
│ (Python API) │ Runs on Windows PC (RTX 2080), tunneled via SSH
└────────┬─────────┘ to localhost:11434
│ (fallback if text < 10 clean chars)
┌──────────────────┐ Stage 2: OCR (Tesseract)
│ Tesseract OCR │ Fallback — works on clean, high-contrast labels
│ (pytesseract) │ Uses `--psm 6` (uniform block of text)
└────────┬─────────┘
┌──────────────────┐ Stage 3: Identifier Extraction
│ Normalize & │ - Strips label prefixes (S/N:, ID#, Machine ID, etc.)
│ Extract IDs │ - Removes punctuation, uppercases
└────────┬─────────┘ - Extracts full lines + individual tokens
┌──────────────────┐ Stage 4: DB Cross-Reference
│ Match against │ - Matches normalized IDs against serial_number,
│ assets.db │ machine_id, connect_id, equipment_id, barcode
└────────┬─────────┘ - Deduplicates by asset id
┌──────────────────┐ Stage 5: Auto-Update
│ Write Back to │ - photo_path → set on matched assets (if blank)
│ Matched Assets │ - serial_number → extracted from OCR text (if blank)
└──────────────────┘ - GPS coords → set on matched asset (if blank, from EXIF)
```
## API Endpoints
### POST `/api/ocr` — Full OCR pipeline
Accepts an image upload (multipart/form-data). Returns extracted text, matched assets, and auto-updates the database.
**Request:**
```
POST /api/ocr
Content-Type: multipart/form-data
file: <image> # Required: the sticker photo
exif_data: <JSON string> # Optional: client-side EXIF data for gallery uploads
```
**Response fields:**
| Field | Description |
|-------|-------------|
| `raw_text` | Raw text extracted by Ollama or Tesseract |
| `ocr_source` | `"ollama"`, `"tesseract"`, or `"none"` |
| `machine_id` | 5-digit machine ID from Connect-ID pattern (XXXXX-XXXXXX) |
| `confidence` | `"high"` (exact pattern match), `"low"` (loose match), `"none"` |
| `matched_assets` | Array of assets matched via identifier cross-reference |
| `exif_gps` | GPS coordinates from photo EXIF (if present) |
| `gps_saved` | `true` if GPS was auto-saved to the matched asset |
| `path` | Saved photo URL path (when `exif_data` provided) |
| `photo_saved` | Number of matched assets whose `photo_path` was auto-updated |
| `serial_saved` | Serial number value that was auto-saved to blank-serial matched assets |
### POST `/api/upload/photo` — Photo-only upload
Saves a photo without OCR. Returns the saved path and any EXIF GPS data.
Use this when the photo was already matched to an asset client-side.
### POST `/api/match-text` — Text matching only
Accepts raw text and finds matching assets. Does not run OCR.
Use for barcode scanner input, QR codes, or client-side vision results.
## Auto-Update Logic
When a photo is uploaded through `/api/ocr` and matches database assets:
1. **photo_path** — If the matched asset has no `photo_path` set, the saved photo's URL path is written to the asset. This links the photo directly to the asset record for display in the asset detail view.
2. **serial_number** — If the matched asset has a blank `serial_number` and the OCR text contains a serial-number pattern (prefixed with S/N, Serial#, Equipment ID, etc.), the extracted serial is written to all matched blank-serial assets.
3. **GPS coordinates** — If the photo has EXIF GPS data and the matched asset has no lat/lng coordinates, they are auto-saved.
All auto-updates are best-effort and non-critical — they don't fail the OCR endpoint if a DB write errors.
## Backfill Script
### `scripts/backfill_vision_photos.py`
Processes all photos in `uploads/photos/` that aren't already linked to an asset via `photo_path`. Useful for:
- Processing photos that were uploaded before the auto-link feature was added
- Retrying photos that failed to match initially
- Bulk-linking a directory of asset photos
```bash
# Dry-run: see what would be updated
python3 scripts/backfill_vision_photos.py
# Apply changes
python3 scripts/backfill_vision_photos.py --apply
# Force overwrite existing photo_path values
python3 scripts/backfill_vision_photos.py --apply --force
```
Options:
| Flag | Description |
|------|-------------|
| `--apply` | Write changes to DB (default is dry-run) |
| `--force` | Overwrite existing `photo_path` values on assets |
| `--db` | Custom database path |
| `--photos-dir` | Custom photos directory |
## `scripts/match_label_photo.py`
CLI utility to OCR a single image and display matched assets. Supports `--text` flag to skip OCR and pass raw text directly.
```bash
# OCR a photo
python3 scripts/match_label_photo.py path/to/photo.jpg
# Match against pre-extracted text
python3 scripts/match_label_photo.py --text "S/N: 2500.0100.0025534"
```
## OCR Services
### Primary: Ollama Vision (qwen2.5vl:3b)
- **Host:** Windows gaming PC (RTX 2080), tunneled to `localhost:11434`
- **Accuracy:** High — can read text from complex labels, dark backgrounds, angled photos
- **Speed:** ~5-15 seconds per image (depends on GPU load)
- **Endpoint:** `/api/generate` with prompt focused on text extraction
### Fallback: Tesseract OCR
- **Installation:** `sudo apt-get install -y tesseract-ocr`
- **Python:** `pip install pytesseract Pillow`
- **Accuracy:** Good on clean, high-contrast, well-lit labels
- **Mode:** `--psm 6` (assume uniform block of text)
### Availability Check
The server checks both services at startup:
- `_HAS_OLLAMA` — set if `http://127.0.0.1:11434/api/generate` responds
- `_HAS_TESSERACT` — set if `pytesseract` is importable
## Identifier Matching
The `normalize_identifier()` function in `classify_makes.py` handles identifier normalization:
1. Strips label prefixes (`S/N:`, `ID#`, `Machine ID`, `Monyx ID`, etc.)
2. Removes dots, dashes, spaces, slashes, colons
3. Uppercases everything
4. Returns just the alphanumeric core
This normalized string is then searched across `serial_number`, `connect_id`, `equipment_id`, `machine_id`, and `barcode` columns in the assets table.
## Photo Storage
- **Directory:** `uploads/photos/`
- **Naming:** UUID hex (avoids filename collisions)
- **Extensions:** `.png`, `.jpg`, `.jpeg`, `.dng` (configurable via `PHOTO_ALLOWED_EXTS`)
- **Max size:** 20 MB (configurable via `PHOTO_MAX_SIZE`)
- **Serving:** Mounted at `/uploads` via FastAPI StaticFiles
- **EXIF round-trip:** Client reads EXIF before upload, server re-embeds it into saved JPEG