Files
boc/projects/landvex/vision/embeddings/ocr_pipeline.py
T
Bernt f4f853d94b landvex: Datafabrik + Vision + Infrastruktur för 50GB skalning
Datafabrik:
- Skördare: crawler, källvitlista, upphandlingsskördare
- Extraktor: LLM-baserad schemastyrd extraktion
- Upplösare: Entitetsupplösning och deduplicering
- Köer: Schemalagd / kunddriven / fält
- Agentorkestrering: 20+ parallella agenter

Vision:
- Identify-modell: ResNet50 + kontrastivt lärande
- Träningspipeline: NT-Xent loss
- Vektordatabas: FAISS för snabb sökning
- OCR-pipeline: Typskyltsläsning

Infrastruktur:
- Docker Compose production
- Terraform för AWS ECS
- Prometheus + Grafana monitorering
- Neo4j + FAISS + MinIO + Redis
2026-07-05 06:25:51 +00:00

180 lines
6.0 KiB
Python

#!/usr/bin/env python3
"""
Landvex Vision — OCR-pipeline för typskyltar
Extraherar tillverkare, modell, serienummer från foton.
"""
import json
from pathlib import Path
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass
import pytesseract
from PIL import Image
import cv2
import numpy as np
@dataclass
class OCRResultat:
text: str
konfidens: float
bbox: Tuple[int, int, int, int] # x, y, w, h
falt: Dict[str, str] # Extraherade fält
class LandvexOCR:
"""OCR-pipeline optimerad för infrastrukturtypskyltar."""
def __init__(self, tesseract_cmd: Optional[str] = None):
if tesseract_cmd:
pytesseract.pytesseract.tesseract_cmd = tesseract_cmd
# Fältmönster per objektklass
self.falt_monster = {
"LVX-ELN-0101": { # Kabelskåp
"tillverkare": r"(ABB|Schneider|Siemens|GE)\s*[\w-]*",
"modell": r"(CDC|Prisma|Okken|IM\s*\w+)",
"ar": r"20\d{2}",
},
"LVX-TRP-0102": { # Vägbelysningsarmatur
"tillverkare": r"(Philips|Thorn|Schreder|Acuity|Cree)",
"modell": r"(SL-\w+|ER\w+|Vista\w+)",
"effekt": r"(\d+)\s*W",
},
"LVX-VAT-0101": { # Brunnsbetäckning
"tillverkare": r"(ULMA|ACO|Wrede|GDK)",
"klass": r"(A15|B125|C250|D400|E600|F900)",
"material": r"(gjutjärn|stål|komposit|betong)",
},
}
def forbehandla_bild(self, bild: np.ndarray) -> np.ndarray:
"""Förbättra bildkvalitet för OCR."""
# Konvertera till gråskala
if len(bild.shape) == 3:
gray = cv2.cvtColor(bild, cv2.COLOR_RGB2GRAY)
else:
gray = bild
# Brusreducering
denoised = cv2.fastNlMeansDenoising(gray)
# Kontrastförbättring (CLAHE)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(denoised)
# Skärpa
kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
sharpened = cv2.filter2D(enhanced, -1, kernel)
return sharpened
def hitta_text_regioner(self, bild: np.ndarray) -> List[Tuple[int, int, int, int]]:
"""Hitta regioner som troligen innehåller text."""
# MSER (Maximally Stable Extremal Regions)
mser = cv2.MSER_create()
regions, _ = mser.detectRegions(bild)
# Filtrera små regioner
h, w = bild.shape
min_area = (h * w) * 0.001 # Minst 0.1% av bilden
bboxes = []
for region in regions:
x, y, w, h = cv2.boundingRect(region)
if w * h > min_area and w > h * 2: # Text är oftast bredare än hög
bboxes.append((x, y, w, h))
return bboxes
def kora_ocr(self, bild: Image.Image, lvx_id: Optional[str] = None) -> List[OCRResultat]:
"""Kör OCR på bild och extrahera fält."""
# Konvertera till numpy
img_array = np.array(bild)
# Förbehandla
processed = self.forbehandla_bild(img_array)
# Hitta textregioner
regioner = self.hitta_text_regioner(processed)
resultat = []
for x, y, w, h in regioner[:5]: # Max 5 regioner
# Beskär region
roi = processed[y:y+h, x:x+w]
# Kör Tesseract
text = pytesseract.image_to_string(roi, lang='eng+swe')
conf_data = pytesseract.image_to_data(roi, output_type=pytesseract.Output.DICT)
# Beräkna medelkonfidens
konfidenser = [c for c in conf_data['conf'] if c > 0]
medel_konf = np.mean(konfidenser) if konfidenser else 0
# Extrahera fält om vi vet objektklassen
falt = {}
if lvx_id and lvx_id in self.falt_monster:
import re
for falt_namn, monster in self.falt_monster[lvx_id].items():
match = re.search(monster, text, re.IGNORECASE)
if match:
falt[falt_namn] = match.group(1)
resultat.append(OCRResultat(
text=text.strip(),
konfidens=medel_konf / 100.0, # Normalisera till 0-1
bbox=(x, y, w, h),
falt=falt
))
# Sortera efter konfidens
resultat.sort(key=lambda x: x.konfidens, reverse=True)
return resultat
def extrahera_falt(self, text: str, lvx_id: str) -> Dict[str, str]:
"""Extrahera strukturerade fält från OCR-text."""
import re
falt = {}
monster = self.falt_monster.get(lvx_id, {})
for falt_namn, pattern in monster.items():
matches = re.findall(pattern, text, re.IGNORECASE)
if matches:
falt[falt_namn] = matches[0]
return falt
def main():
"""Demo: OCR på syntetisk bild."""
print("🔤 Landvex OCR Pipeline")
ocr = LandvexOCR()
# Skapa syntetisk testbild med text
from PIL import ImageDraw, ImageFont
img = Image.new('RGB', (400, 200), color='white')
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 24)
except:
font = ImageFont.load_default()
draw.text((20, 20), "ABB Kabeldon", fill='black', font=font)
draw.text((20, 60), "CDC-LT 400", fill='black', font=font)
draw.text((20, 100), "2023", fill='black', font=font)
# Kör OCR
resultat = ocr.kora_ocr(img, lvx_id="LVX-ELN-0101")
print("\n🎯 OCR-resultat:")
for i, r in enumerate(resultat[:3]):
print(f" Region {i+1}:")
print(f" Text: {r.text[:100]}")
print(f" Konfidens: {r.konfidens:.2f}")
print(f" Fält: {r.falt}")
print("\n✅ OCR klar!")
if __name__ == "__main__":
main()