17b870e4cc
- 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
175 lines
7.3 KiB
Markdown
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
|