Files
timing/backend/ocr.py
T
steinhelge 330ba7a93d Initial commit: MVP tidtakingssystem
- Backend: FastAPI, EXIF-parser, EasyOCR, SQLite
- Frontend: React admin (startliste, passeringer, gjennomgang, resultater)
- Docker: docker-compose med depot/processed/data-volumer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 15:01:33 +01:00

109 lines
3.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Startnummer-deteksjon og OCR.
Bruker EasyOCR for å lese sifre fra bib-nummerlapp.
Returnerer en OcrResult med tall, konfidens og partial-flagg.
"""
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
import cv2
import numpy as np
# EasyOCR lastes lazy for å unngå lang oppstartstid
_reader = None
def _get_reader():
global _reader
if _reader is None:
import easyocr
_reader = easyocr.Reader(["en"], gpu=False, verbose=False)
return _reader
@dataclass
class OcrResult:
digits: Optional[str] # Gjenkjente sifre, f.eks. "42", None hvis ingen
confidence: float # 0.01.0
partial: bool # True hvis nummeret trolig er delvis skjult
raw_texts: list[str] = field(default_factory=list) # Alle OCR-treff for debug
def _preprocess(image_path: Path) -> np.ndarray:
"""Forbered bilde for OCR: gråskala, kontrast, skarphet."""
img = cv2.imread(str(image_path))
if img is None:
raise ValueError(f"Kunne ikke lese bilde: {image_path}")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# CLAHE for bedre kontrast i varierende lys
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(gray)
# Skaler opp hvis bildet er lite (OCR er bedre på større tekst)
h, w = enhanced.shape
if max(h, w) < 800:
scale = 800 / max(h, w)
enhanced = cv2.resize(
enhanced, (int(w * scale), int(h * scale)),
interpolation=cv2.INTER_CUBIC,
)
return enhanced
def _extract_bib_number(texts: list[tuple]) -> tuple[Optional[str], float, bool]:
"""
Finn beste siffersekvens blant OCR-treff.
Returnerer (sifre, konfidens, partial).
"""
candidates = []
for (_, text, conf) in texts:
# Behold kun sifre
digits = re.sub(r"[^0-9]", "", text)
if digits:
candidates.append((digits, float(conf)))
if not candidates:
return None, 0.0, False
# Velg kandidat med høyest konfidens
best_digits, best_conf = max(candidates, key=lambda x: x[1])
# Heuristikk: 12 sifre kan tyde på delvis synlig nummer
partial = len(best_digits) < 2
return best_digits, best_conf, partial
def read_bib(image_path: Path) -> OcrResult:
"""
Les startnummer fra bildet.
Returnerer OcrResult. Aldri exception — fallback til konfidens 0 ved feil.
"""
try:
processed = _preprocess(image_path)
reader = _get_reader()
results = reader.readtext(processed, detail=1, paragraph=False)
raw_texts = [text for (_, text, _) in results]
digits, confidence, partial = _extract_bib_number(results)
return OcrResult(
digits=digits,
confidence=confidence,
partial=partial,
raw_texts=raw_texts,
)
except Exception as e:
return OcrResult(
digits=None,
confidence=0.0,
partial=False,
raw_texts=[f"ERROR: {e}"],
)