Specifica pipeline realtime flywms

Obiettivo
Costruire una pipeline realtime modulare per video/camera, con tre blocchi principali:

Capture/Preview
YOLO Detection
OCR

I blocchi lavorano in parallelo, ognuno al proprio ritmo. Se un blocco e' lento, non deve bloccare gli altri. I frame/ROI vecchi possono essere scartati per mantenere bassa la latenza.

Principio chiave
Non elaboriamo necessariamente tutti i frame. Elaboriamo il piu' recente disponibile quando un worker e' libero.

meglio saltare frame vecchi
che accumulare ritardo

Strutture dati
FramePacket
  frame_id: intero crescente
  timestamp: tempo acquisizione
  frame: immagine OpenCV BGR
  width: larghezza
  height: altezza
  source: video/camera

Detection
  class_id
  class_name
  confidence
  bbox: x1, y1, x2, y2

DetectionResult
  frame_id
  timestamp
  detections: lista Detection
  inference_ms
  source_width
  source_height

RoiPacket
  roi_id
  source_frame_id
  timestamp
  class_name
  confidence
  bbox
  roi_image: crop BGR copiato
  width
  height

OcrResult
  roi_id
  source_frame_id
  timestamp
  text
  raw_text
  confidence opzionale
  bbox
  ocr_ms

Buffer
Usiamo buffer thread-safe "latest", non FIFO classici.

LatestBuffer
  max_size
  put(item)
  get_latest_blocking(stop_event)
  get_latest_nowait()
  stats()

Comportamento:

put(item):
  se pieno, elimina elementi vecchi
  inserisce item nuovo
  notifica i worker in attesa

get_latest_blocking():
  se vuoto, aspetta
  quando c'e' almeno un item:
    prende il piu' recente
    scarta gli elementi piu' vecchi
    restituisce item

Buffer previsti:

frame_buffer: FramePacket
roi_buffer: RoiPacket

Risultati condivisi:

SharedState
  latest_detection_result
  latest_ocr_results
  counters/statistiche

SharedState va protetto con lock breve.

Thread
Main/Capture Thread
  legge frame
  genera FramePacket
  mette FramePacket in frame_buffer
  visualizza preview
  opzionalmente disegna ultimi risultati disponibili

YOLO Worker Thread
  aspetta ultimo FramePacket
  copia il frame fuori lock
  esegue detection
  salva DetectionResult in SharedState
  crea RoiPacket per le etichette utili
  mette RoiPacket in roi_buffer
  aggiorna statistiche

OCR Worker Thread
  aspetta ultimo RoiPacket
  analizza ROI
  salva OcrResult in SharedState/log
  aggiorna statistiche

Thread safety
Il lock deve proteggere solo operazioni piccole:

inserire riferimento nel buffer
leggere riferimento dal buffer
aggiornare contatori
aggiornare latest result
notify/wait

Il lock non deve coprire:

cap.read()
frame.copy()
YOLO forward
OCR
cv2.imshow
conversioni immagine

Politica copie
Prima versione, robusta:

Capture:
  non copia ogni frame
  mette il frame letto nel FramePacket

YOLO:
  prende FramePacket
  fa frame.copy() solo del frame che analizzera'

OCR:
  riceve ROI gia' copiata

Questo evita corruzione dati e mantiene copie limitate.

Politica realtime
Se YOLO e' lento:

non analizza frame arretrati
prende il piu' recente disponibile

Se OCR e' lento:

non legge ROI vecchie
prende la ROI piu' recente disponibile

Questo riduce la latenza.

Algoritmo pseudocodice - main/capture
inizializza config
carica labels
carica modello YOLO
apri video/camera

crea stop_event
crea frame_buffer max_size=N
crea roi_buffer max_size=M
crea shared_state

avvia thread yolo_worker
avvia thread ocr_worker

frame_id = 0

loop main:
  grabbed, frame = cap.read()

  se non grabbed:
    stop_event.set()
    break

  frame_id += 1

  packet = FramePacket(
    frame_id=frame_id,
    timestamp=now(),
    frame=frame,
    width=frame.width,
    height=frame.height,
    source=source_name
  )

  frame_buffer.put(packet)

  detection_result = shared_state.get_latest_detection()
  ocr_results = shared_state.get_recent_ocr_results()

  display_frame = frame.copy()
  draw detections compatibili su display_frame
  draw OCR texts compatibili su display_frame

  cv2.imshow("Capture", display_frame)

  se premuto q:
    stop_event.set()
    break

attendi chiusura worker
rilascia video
chiudi finestre

Algoritmo pseudocodice - YOLO worker
function yolo_worker():
  while not stop_event:
    packet = frame_buffer.get_latest_blocking(stop_event)

    se packet e' None:
      continue

    local_frame = packet.frame.copy()

    t0 = now()
    detections = run_yolo(local_frame)
    inference_ms = now() - t0

    result = DetectionResult(
      frame_id=packet.frame_id,
      timestamp=now(),
      detections=detections,
      inference_ms=inference_ms,
      source_width=packet.width,
      source_height=packet.height
    )

    shared_state.set_latest_detection(result)

    per ogni detection in detections:
      se detection.class_name non e' "etichetta":
        continue

      se bbox troppo piccola:
        continue

      se OCR cooldown per questa zona non e' scaduto:
        continue

      roi = crop local_frame usando bbox espansa
      roi_copy = roi.copy()

      roi_packet = RoiPacket(
        roi_id=next_roi_id(),
        source_frame_id=packet.frame_id,
        timestamp=now(),
        class_name=detection.class_name,
        confidence=detection.confidence,
        bbox=detection.bbox,
        roi_image=roi_copy,
        width=roi_copy.width,
        height=roi_copy.height
      )

      roi_buffer.put(roi_packet)

    aggiorna statistiche yolo

Algoritmo pseudocodice - OCR worker
function ocr_worker():
  while not stop_event:
    roi_packet = roi_buffer.get_latest_blocking(stop_event)

    se roi_packet e' None:
      continue

    t0 = now()
    processed = preprocess_for_ocr(roi_packet.roi_image)
    text, raw_text = run_ocr(processed)
    ocr_ms = now() - t0

    result = OcrResult(
      roi_id=roi_packet.roi_id,
      source_frame_id=roi_packet.source_frame_id,
      timestamp=now(),
      text=text,
      raw_text=raw_text,
      bbox=roi_packet.bbox,
      ocr_ms=ocr_ms
    )

    shared_state.add_ocr_result(result)

    se text valido:
      logga risultato

    aggiorna statistiche ocr

LatestBuffer pseudocodice
class LatestBuffer:
  items = deque(maxlen=max_size)
  condition = Condition()
  pushed = 0
  popped = 0
  dropped = 0

  put(item):
    with condition:
      before = len(items)

      se before == max_size:
        dropped += 1

      items.append(item)
      pushed += 1
      condition.notify_all()

  get_latest_blocking(stop_event):
    with condition:
      while items e' vuoto and not stop_event.is_set():
        condition.wait(timeout=0.1)

      se stop_event.is_set():
        return None

      latest = items[-1]
      skipped = len(items) - 1
      dropped += skipped
      items.clear()
      popped += 1

      return latest

SharedState pseudocodice
class SharedState:
  lock
  latest_detection_result
  ocr_results_deque maxlen=100
  stats

  set_latest_detection(result):
    with lock:
      latest_detection_result = result

  get_latest_detection():
    with lock:
      return latest_detection_result

  add_ocr_result(result):
    with lock:
      ocr_results_deque.append(result)

  get_recent_ocr_results():
    with lock:
      return copia lista risultati recenti

Statistiche minime
Da stampare ogni N secondi:

capture_fps
yolo_fps
ocr_fps
frame_buffer pushed/popped/dropped
roi_buffer pushed/popped/dropped
avg_yolo_ms
avg_ocr_ms
last_frame_id
last_yolo_frame_id
last_ocr_frame_id
latency_yolo_ms
latency_ocr_ms

Configurazione iniziale
--video testhd.mp4
--frame-buffer-size 10
--roi-buffer-size 20
--min-confidence 0.30
--label-class etichetta
--max-roi-per-frame 2
--ocr-min-digits 2
--preview-width 1280
--backend cpu|cuda|cuda-fp16
--debug-yolo-window
--debug-ocr-window

Prima versione: cosa evitare
Per la prima versione eviterei:

Dear PyGui
PaddleOCR
modello YOLO moderno
tracking complesso

Prima validiamo la pipeline con OpenCV window + YOLO attuale + OCR attuale. Poi sostituiamo i motori uno alla volta.

Decisione architetturale
Questa pipeline privilegia:

bassa latenza
fluidita' realtime
uso controllato della memoria
misurabilita'

e sacrifica:

analisi completa di ogni frame
ordine perfetto di output per ogni frame

Per un sistema operativo realtime e' il compromesso giusto.