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>
This commit is contained in:
2026-03-20 15:01:33 +01:00
commit 330ba7a93d
35 changed files with 5038 additions and 0 deletions
+270
View File
@@ -0,0 +1,270 @@
# Bildedrevet tidtakingssystem — teknisk spesifikasjon
## Oversikt
Et tidtakingssystem for løp uten elektroniske brikker. Utøvere identifiseres ved hjelp av bilder tatt ved start, passeringspunkter og mål. Bildene inneholder nøyaktig tidsstempel og GPS-posisjon i EXIF-metadata, noe som gjør det mulig å rekonstruere hver utøvers rute og passeringstidspunkter langs løypa.
Identifikasjon skjer ved kombinasjon av to metoder:
1. OCR på startnummer
2. Biometrisk persongjenkjenning (ansikt/kroppsform)
De to metodene forsterker hverandre — et delvis skjult startnummer kan disambigueres av persongjenkjenning, og omvendt.
---
## Kamerastasjoner
Kamera settes opp ved:
- **Start** — alle utøvere fotograferes ved avgang
- **Passeringspunkter** — vilkårlig antall stasjoner langs løypa (N ≥ 0)
- **Mål** — alle utøvere fotograferes ved ankomst
Hvert kamera må:
- Produsere JPEG/PNG med korrekt EXIF-data (se nedenfor)
- Ha burst-modus eller høy framerate for å håndtere bevegelsesuskarphet
- Synkroniseres mot NTP (nøyaktig klokkeslett er kritisk)
### EXIF-krav per bilde
Alle bilder MÅ ha følgende EXIF-felter satt:
| EXIF-felt | Innhold | Merknad |
|---|---|---|
| `DateTimeOriginal` | ISO 8601 UTC | Eks: `2024-06-15T10:23:45.123Z` |
| `SubSecTimeOriginal` | Millisekunder | For sub-sekund presisjon |
| `GPSLatitude` + `GPSLatitudeRef` | Desimalgrader | Stasjonens posisjon |
| `GPSLongitude` + `GPSLongitudeRef` | Desimalgrader | Stasjonens posisjon |
| `GPSAltitude` | Meter over havet | Valgfritt men anbefalt |
| `CameraLabel` / `ImageDescription` | Stasjonsnavn | Eks: `"start"`, `"cp1"`, `"finish"` |
EXIF-posisjonen representerer stasjonens faste posisjon (ikke utøverens bevegelse). For mobile stasjoner settes GPS per bilde fra kamerakontrollerens GPS-mottaker.
---
## Dataflyt og pipeline
```
Bilde (JPEG + EXIF)
[1] Inntak og preprocessing
- Valider EXIF (tid + GPS påkrevd, avvis ellers)
- Kø-basert mottak fra alle stasjoner
- Generer utsnitt av utøver (bounding box)
- Kvalitetsfiltrering: skarphet, lysforhold, utøver synlig
├──────────────────────────────────┐
▼ ▼
[2a] Startnummer-OCR [2b] Persongjenkjenning
- Detekter nummerlapp - Generer embedding fra
- Les sifre ansikt og/eller kropp
- Returner: sifre[], konfidens, - Match mot profil-DB
andel synlig (0.01.0) - Returner: profil_id,
konfidens, match-type
│ │
└──────────────┬───────────────────┘
[3] Konfidensbasert fusjonsmotor
- Kombiner OCR-resultat og personmatch
- Beslutningstre (se nedenfor)
- Output: identifikasjon + konfidensscore
├── Høy konfidens ──► [4a] Logg passering direkte
└── Lav konfidens ──► [4b] Flagg for manuell gjennomgang
[5] Resultatdatabase
- Utøver-ID, stasjon, tidsstempel (fra EXIF), GPS-pos
- Beregn split-tider og løyperute
```
---
## Fusjonslogikk
Prioriteringsrekkefølge ved konflikt:
```
1. Startnummer tydelig (konfidens > 0.90)
→ Bruk startnummer, ignorer person-match hvis konflikt
2. Startnummer delvis synlig (konfidens 0.500.90) + person-match
→ Kombiner: filtrer kandidater fra startnummer, disambiguer med person
→ Logg hvis entydig, flagg hvis flere kandidater gjenstår
3. Startnummer uleselig (konfidens < 0.50) + person-match (konfidens > 0.70)
→ Bruk person-match alene
→ Logg med merknad "number_unreadable"
4. Ingen av delene gir tilstrekkelig konfidens
→ Flagg for manuell gjennomgang med bilde og metadata
```
Minste akseptable konfidens for automatisk logging: konfigurbart, anbefalt startverdi `0.75`.
---
## Profildatabase (utøver-DB)
Utøverprofiler bygges opp suksessivt:
- **Bootstrapping:** Startkameraet genererer en profil for hver unik person som passerer. Profil-ID tildeles automatisk.
- **Kobling til startliste:** Valgfritt — kan mates inn med navn + startnummer + registreringsbilde. Kobles til profil ved første gjenkjenning.
- **Uten startliste:** Systemet fungerer fullt ut; utøvere får anonyme ID-er. Startliste kan legges til i etterkant for å sette navn.
### Skjema (forenklet)
```json
{
"profile_id": "uuid",
"bib_number": "42",
"name": "Ola Nordmann",
"embeddings": [ /* array av vektorer fra registreringsbilder */ ],
"registration_images": [ "path/to/img1.jpg" ],
"created_at": "2024-06-15T09:00:00Z"
}
```
---
## Passeringslogg
En passering lagres når en utøver identifiseres ved en stasjon:
```json
{
"passage_id": "uuid",
"profile_id": "uuid",
"bib_number": "42",
"station": "cp1",
"timestamp_utc": "2024-06-15T10:23:45.123Z",
"gps_lat": 61.1234,
"gps_lon": 10.5678,
"gps_alt": 910,
"confidence": 0.92,
"id_method": "bib_ocr+person",
"source_image": "path/to/image.jpg",
"needs_review": false
}
```
Fra passeringsloggen beregnes:
- Split-tider mellom stasjoner
- Totaltid start→mål
- Løyperute (GPS-spor av stasjonspasseringer)
- Estimert posisjon i løypa på ethvert tidspunkt (interpolasjon)
---
## Komponenter som skal implementeres
### Backend (Python anbefalt)
| Modul | Ansvar |
|---|---|
| `ingest.py` | Ta imot bilder, valider EXIF, kø til pipeline |
| `exif_parser.py` | Parse og normaliser EXIF-metadata |
| `ocr.py` | Startnummer-deteksjon og OCR |
| `recognition.py` | Persongjenkjenning, embedding, match mot profil-DB |
| `fusion.py` | Kombiner OCR + person, beslutningslogikk |
| `profile_db.py` | CRUD for utøverprofiler og embeddings |
| `passage_log.py` | Skriv og query passeringslogg |
| `results.py` | Beregn split-tider, totalresultat, løyperute |
| `review_queue.py` | Håndter manuelle gjennomganger |
### Foreslåtte biblioteker
- **OCR:** `easyocr` eller `paddleocr` (god på korte numre i varierende lysforhold)
- **Persongjenkjenning:** `deepface` eller `insightface` (lokalt, ingen sky-avhengighet)
- **EXIF-parsing:** `piexif` eller `exifread`
- **Database:** SQLite for enkel lokal drift, PostgreSQL for produksjon
- **Bildeprosessering:** `opencv-python`, `Pillow`
- **Kø:** `redis` + `rq` for asynkron pipeline, eller enkel filkø for MVP
### Frontend (valgfritt, React anbefalt)
- Live-visning av passeringer
- Leaflet-kart med GPS-posisjoner til stasjoner og utøveres rute
- Resultatliste med split-tider
- Manuell gjennomgangskø med bildevisning
---
## Kanttilfeller og håndtering
| Situasjon | Håndtering |
|---|---|
| Utøvere overlapper i bildet | Generer separate bounding boxes per utøver |
| Startnummeret er delvis bak arm/klesplagg | OCR returnerer synlige sifre + `partial: true` |
| Samme utøver fanges i burst-sekvens | Deduplisering: behold beste bilde per utøver per stasjon (±2 sek vindu) |
| Refleks/solblending gjør bildet ubrukelig | Kvalitetsfilter forkaster bildet; neste i burst brukes |
| EXIF mangler tid eller GPS | Bildet avvises med feillogg; manuell stasjon-tid kan angis som fallback |
| Ukjent person, uleselig nummer | Flagges med bilde til manuell gjennomgang |
| To utøvere med like startnummer (feil) | Flagges som konflikt; begge profiler merkes |
---
## Personvern
- Biometrisk gjenkjenning krever samtykke (GDPR artikkel 9 — særlig kategori)
- Anbefalt løsning: opt-in ved påmelding, registreringsbilde lastes opp i forkant
- Embeddings lagres, ikke råbilder, etter at passering er bekreftet (konfigurerbart)
- Råbilder bør slettes etter løpet med mindre deltaker har samtykket til lagring
- Systemet bør kunne kjøres i "bib-only"-modus uten persongjenkjenning for arrangement som ikke innhenter biometrisk samtykke
---
## Infrastruktur
### Container-drift
Hele systemet kjøres i Docker (docker-compose):
- **backend** — Python/FastAPI, pipeline og API
- **frontend** — React admin-grensesnitt (Nginx)
- **db** — SQLite via volum (PostgreSQL i produksjon)
Volumes:
- `./depot` — inntakskatalog for nye bilder (kamera-drop)
- `./processed` — bearbeidede bilder med unike filnavn
- `./data` — SQLite-database
### Bildehåndtering
1. Kamera/bruker legger bilder i `depot/`
2. `ingest.py` oppdager nye filer (inotify / polling)
3. EXIF valideres; ved godkjenning:
- Metadata lagres i DB
- Filen flyttes til `processed/<år>/<måned>/<uuid>_<originalfilnavn>`
- Depot-filen slettes
4. Ved ugyldig EXIF: filen flyttes til `depot/rejected/` med feillogg
---
## Admin-webgrensesnitt (React)
Tilgjengelig via nettleser, kommuniserer med backend via REST API.
| Skjerm | Funksjonalitet |
|---|---|
| Startliste | Last opp CSV, vis/rediger utøverliste |
| Live passeringer | Strøm av nylige passeringer fra alle stasjoner |
| Manuell gjennomgang | Vis bilder med lav konfidens, bekreft/korriger ID |
| Resultater | Split-tider og totalresultat per utøver |
| Stasjoner | Oversikt over kamerastasjoner og siste aktivitet |
---
## MVP-avgrensning (fase 1)
For å komme raskt i gang, begrens til:
1. EXIF-parsing og validering
2. Startnummer-OCR (ingen persongjenkjenning ennå)
3. Manuell startliste som CSV (bib_number → navn)
4. Passeringslogg til SQLite
5. REST API (FastAPI) for frontend
6. React admin-grensesnitt: startliste-import og manuell gjennomgang
7. Docker-oppsett med depot/processed-volumer
Persongjenkjenning og live Leaflet-kart legges til i fase 2.