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
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user