Cross-reference photos against assets DB
- Scanned all 15 photos in uploads/photos/ via vision + Tesseract OCR - Matched keurig_label.jpg → Asset #5144 (machine_id 636671) - Updated Asset #5144 photo_path in assets.db - Created scripts/crossref_photos.py for reusable batch matching - Reported unmatched IDs for coca_cola_label.jpg and telemetry_device.jpg - Generated crossref_report.md with full findings
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
# Photo Cross-Reference Report
|
||||||
|
**Generated:** 2026-05-29
|
||||||
|
|
||||||
|
## Database
|
||||||
|
- assets.db: 7,488 assets
|
||||||
|
|
||||||
|
## Photos Scanned
|
||||||
|
15 files in uploads/photos/
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
### Matched (1 photo → 1 asset update)
|
||||||
|
|
||||||
|
| Photo | Asset ID | Machine ID | Name | Action |
|
||||||
|
|-------|----------|------------|------|--------|
|
||||||
|
| keurig_label.jpg | 5144 | 636671 | 636671 / 956 Cypress Way | ✅ photo_path set to /uploads/photos/keurig_label.jpg |
|
||||||
|
|
||||||
|
### Unmatched — For Investigation (2 photos)
|
||||||
|
|
||||||
|
**coca_cola_label.jpg** — Coca-Cola branded label
|
||||||
|
- Extracted IDs: RY10006338, RVCC-660-8, 201037BA00039
|
||||||
|
- RY10006338 is Royal-pattern serial but not in DB
|
||||||
|
- 201037BA00039 is Royal serial format; closest DB match: Asset #6815 (A201037BA00030)
|
||||||
|
- Likely a pulled/retired or unregistered machine
|
||||||
|
|
||||||
|
**telemetry_device.jpg** — 4G telemetry device
|
||||||
|
- Extracted IDs: 4434331624226353 (ICCID), 48602143 (Monyx ID)
|
||||||
|
- These are telemetry/sim identifiers, not vending machine assets
|
||||||
|
- Consider tracking in a separate telemetry devices table
|
||||||
|
|
||||||
|
### Test Data (3 photos)
|
||||||
|
- 557ba8d1..., 59dd8174..., a8c7e553...: "Machine 1D 12845-678901" — app test pattern, no DB match
|
||||||
|
|
||||||
|
### No Text (9 photos)
|
||||||
|
- Icon-sized images (100x100 to 200x100): no readable text
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
- `/home/oplabs/projects/canteen-asset-tracker/assets.db` — Asset #5144 photo_path updated
|
||||||
|
- `/home/oplabs/projects/canteen-asset-tracker/scripts/crossref_photos.py` — reusable batch matching script
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Cross-reference extracted photo IDs against assets DB.
|
||||||
|
|
||||||
|
Processes all photos in uploads/photos/, runs vision/OCR text extraction,
|
||||||
|
matches identifiers against assets.machine_id / serial_number / connect_id,
|
||||||
|
updates matched assets with photo_path, and reports unmatched IDs.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 scripts/crossref_photos.py # process all photos
|
||||||
|
python3 scripts/crossref_photos.py --photo <name> # single photo
|
||||||
|
python3 scripts/crossref_photos.py --text "S/N: 1234" # from text only
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
from classify_makes import normalize_identifier, find_asset_by_normalized_id
|
||||||
|
|
||||||
|
DB_PATH = str(Path(__file__).resolve().parent.parent / "assets.db")
|
||||||
|
PHOTOS_DIR = str(Path(__file__).resolve().parent.parent / "uploads" / "photos")
|
||||||
|
|
||||||
|
|
||||||
|
def process_photo(photo_path: str, db_path: str = DB_PATH) -> dict:
|
||||||
|
"""Process a single photo: extract text, match DB, update photo_path."""
|
||||||
|
fname = os.path.basename(photo_path)
|
||||||
|
result = {
|
||||||
|
"photo": fname,
|
||||||
|
"photo_path": f"/uploads/photos/{fname}",
|
||||||
|
"size_bytes": os.path.getsize(photo_path),
|
||||||
|
"vision_text": "",
|
||||||
|
"extracted_ids": [],
|
||||||
|
"matches": [],
|
||||||
|
"updated": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try Tesseract OCR
|
||||||
|
try:
|
||||||
|
import pytesseract
|
||||||
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
|
img = PILImage.open(photo_path)
|
||||||
|
img_gray = img.convert("L")
|
||||||
|
text = pytesseract.image_to_string(img_gray, config="--psm 6")
|
||||||
|
text = text.strip()
|
||||||
|
clean_chars = sum(1 for c in text if c.isalnum() or c in " \n/-.")
|
||||||
|
if text and clean_chars >= 4:
|
||||||
|
result["vision_text"] = text
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not result["vision_text"]:
|
||||||
|
result["vision_source"] = "none"
|
||||||
|
return result
|
||||||
|
|
||||||
|
result["vision_source"] = "tesseract"
|
||||||
|
|
||||||
|
# Extract identifiers from text
|
||||||
|
identifiers = []
|
||||||
|
lines = result["vision_text"].split("\n")
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or len(line) < 4:
|
||||||
|
continue
|
||||||
|
norm = normalize_identifier(line)
|
||||||
|
if norm and len(norm) >= 4:
|
||||||
|
identifiers.append(norm)
|
||||||
|
tokens = re.findall(r"[A-Za-z0-9]{4,}", line)
|
||||||
|
for token in tokens:
|
||||||
|
norm = normalize_identifier(token)
|
||||||
|
if norm and len(norm) >= 4 and norm not in identifiers:
|
||||||
|
identifiers.append(norm)
|
||||||
|
|
||||||
|
result["extracted_ids"] = identifiers
|
||||||
|
|
||||||
|
# Match against DB
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
seen_ids = set()
|
||||||
|
|
||||||
|
for norm_id in identifiers:
|
||||||
|
assets = find_asset_by_normalized_id(db_path, norm_id)
|
||||||
|
for a in assets:
|
||||||
|
if a["id"] not in seen_ids:
|
||||||
|
seen_ids.add(a["id"])
|
||||||
|
result["matches"].append(a)
|
||||||
|
|
||||||
|
# Update photo_path if empty
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT photo_path FROM assets WHERE id = ?", (a["id"],)
|
||||||
|
).fetchone()
|
||||||
|
if existing and not existing["photo_path"]:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE assets SET photo_path = ?, updated_at = datetime('now') WHERE id = ?",
|
||||||
|
(result["photo_path"], a["id"]),
|
||||||
|
)
|
||||||
|
result["updated"] = True
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Cross-reference photo IDs against assets DB"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--photo", "-p", help="Process a single photo by filename"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--text", "-t", help="Process raw text directly (skip OCR)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--db",
|
||||||
|
default=DB_PATH,
|
||||||
|
help=f"Database path (default: {DB_PATH})",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
if args.text:
|
||||||
|
# Process from text directly
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print(f"PROCESSING: supplied text ({len(args.text)} chars)")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
print(f" Text: {args.text[:500]}")
|
||||||
|
|
||||||
|
identifiers = []
|
||||||
|
norm = normalize_identifier(args.text)
|
||||||
|
if norm and len(norm) >= 4:
|
||||||
|
identifiers.append(norm)
|
||||||
|
tokens = re.findall(r"[A-Za-z0-9]{4,}", args.text)
|
||||||
|
for token in tokens:
|
||||||
|
n = normalize_identifier(token)
|
||||||
|
if n and len(n) >= 4 and n not in identifiers:
|
||||||
|
identifiers.append(n)
|
||||||
|
|
||||||
|
print(f"\n Normalized IDs: {identifiers}")
|
||||||
|
conn = sqlite3.connect(args.db)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
for norm_id in identifiers:
|
||||||
|
assets = find_asset_by_normalized_id(args.db, norm_id)
|
||||||
|
if assets:
|
||||||
|
for a in assets:
|
||||||
|
print(f"\n ✓ MATCH → Asset #{a['id']}")
|
||||||
|
print(f" Machine ID: {a['machine_id']}")
|
||||||
|
print(f" Name: {a['name'][:60]}")
|
||||||
|
else:
|
||||||
|
print(f"\n ✗ No match for '{norm_id}'")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.photo:
|
||||||
|
photo_path = os.path.join(PHOTOS_DIR, args.photo)
|
||||||
|
if not os.path.exists(photo_path):
|
||||||
|
print(f"ERROR: {photo_path} not found")
|
||||||
|
sys.exit(1)
|
||||||
|
results.append(process_photo(photo_path, args.db))
|
||||||
|
else:
|
||||||
|
# Process all photos
|
||||||
|
for fname in sorted(os.listdir(PHOTOS_DIR)):
|
||||||
|
photo_path = os.path.join(PHOTOS_DIR, fname)
|
||||||
|
if os.path.isfile(photo_path):
|
||||||
|
results.append(process_photo(photo_path, args.db))
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
matched = [r for r in results if r["matches"]]
|
||||||
|
unmatched = [r for r in results if not r["matches"] and r["vision_text"]]
|
||||||
|
no_text = [r for r in results if not r["vision_text"]]
|
||||||
|
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print(f"CROSS-REFERENCE SUMMARY ({datetime.now().isoformat()})")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
print(f" Photos scanned: {len(results)}")
|
||||||
|
print(f" Photos with text: {len(results) - len(no_text)}")
|
||||||
|
print(f" Matches found: {len(matched)}")
|
||||||
|
print(f" Unmatched: {len(unmatched)}")
|
||||||
|
print(f" No text (small/thumbs): {len(no_text)}")
|
||||||
|
|
||||||
|
for r in matched:
|
||||||
|
for a in r["matches"]:
|
||||||
|
status = "UPDATED" if r["updated"] else "already set"
|
||||||
|
print(
|
||||||
|
f"\n ✓ {r['photo']} → Asset #{a['id']} "
|
||||||
|
f"(machine_id={a['machine_id']}) [{status}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
for r in unmatched:
|
||||||
|
print(f"\n ✗ {r['photo']} — no DB match")
|
||||||
|
print(f" IDs: {r['extracted_ids'][:5]}")
|
||||||
|
print(f" Text preview: {r['vision_text'][:100]}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user