Glimt trasforma un secondo di ogni giorno in un film. La parte interessante è che non devi filmare quel secondo: Glimt lo trova nei video che hai già girato e sceglie il secondo in cui c'è tuo figlio, anche in un video di compleanno affollato con altri cinque bambini. Tutto gira sull'iPhone: niente server, niente account, niente upload. Ecco come funziona, con i modelli, la matematica e tempi reali.
La pipeline
Perché non usare semplicemente Vision di Apple?
Vision qui fa molto: trova i volti (VNDetectFaceCaptureQualityRequest, che valuta anche quanto ogni volto è nitido e frontale), i loro landmark (VNDetectFaceLandmarksRequest) e, su iOS 18, quanto è bello un frame (CalculateImageAestheticsScoresRequest, che segnala anche scontrini e screenshot). Quello che non offre è di chi è un volto. L'album Persone di Photos lo sa, ma quell'indice non è aperto alle app.
La prima versione di Glimt usava VNGenerateImageFeaturePrintRequest su un ritaglio del volto. È un'impronta d'immagine generica: confronta capelli, luce, colori e sfondo tanto quanto il volto stesso. In pratica scambiava tranquillamente fratelli e sorelle, e qualsiasi bambino fotografato nella stessa stanza alla stessa ora del giorno. Era lo strumento sbagliato.
Il modello di riconoscimento facciale
Ora Glimt ha il suo: SFace (Zhong et al., 2021, «Sigmoid-Constrained Hypersphere Loss for Robust Face Recognition»), con un backbone MobileFaceNet, pubblicato nell'OpenCV Zoo con licenza Apache 2.0. Dichiara il 99,40 % su LFW. Prende un volto di 112 × 112 e restituisce 128 numeri, un embedding; due foto della stessa persona danno embedding che puntano nella stessa direzione.
La conversione è stata ONNX → PyTorch (onnx2torch) → Core ML (coremltools, ML Program, float16), poi una quantizzazione lineare dei pesi a 8 bit. Il risultato pesa 9,8 MB, si carica in circa 0,5 s la prima volta e il suo output corrisponde al modello ONNX originale con un coseno di 0,998 su un input di test. La normalizzazione dell'input è dentro il grafo, quindi l'app gli passa semplici pixel RGB da 0 a 255.
L'allineamento conta più di quanto pensi
SFace è stato addestrato su volti allineati a un template fisso: occhi, punta del naso e angoli della bocca in posizioni note nel quadrato di 112 × 112. Un volto inclinato o decentrato dà un embedding sensibilmente peggiore. I landmark di Vision forniscono i cinque punti (le pupille, il centro della regione del naso, i punti più esterni delle labbra); Glimt trova poi la rotazione, la scala uniforme e la traslazione che li portano meglio sul template. In due dimensioni quel problema ai minimi quadrati ha una soluzione in forma chiusa, se tratti i punti come numeri complessi:
a = Σ conj(zᵢ − z̄) · (wᵢ − w̄) / Σ |zᵢ − z̄|² // rotation + scale, one complex number
w = a · z + (w̄ − a · z̄) // the transform for any point z
Core Image la applica (una sola trasformazione affine, renderer software, un output di 112 pixel) e il ritaglio va al modello. Le foto di riferimento vengono elaborate due volte, così come sono e specchiate, e si fa la media delle due: un riferimento un po' più stabile, gratis.
È lo stesso bambino? Calibrare la soglia
L'app confronta l'embedding di ogni volto con le foto di riferimento tramite similarità del coseno e tiene la corrispondenza migliore. Dove tracciare la linea è stato misurato, non indovinato, su ritratti della Casa Bianca di pubblico dominio (ritratti ufficiali a distanza di anni e un ritratto di famiglia con entrambi i genitori e le due figlie):
Lo stesso uomo in ritratti scattati a quattro anni di distanza ottiene 0,83; le stesse persone estratte dalla foto di famiglia 0,70–0,85. Gli estranei restano sotto 0,25. Il caso difficile è la famiglia: due sorelle nello stesso scatto ottengono 0,43 tra loro, madre e figlie fino a 0,41 (stessa luce e stessa posa rendono i parenti più simili), mentre la madre confrontata con le figlie in una foto diversa scende a 0,26–0,30. Quindi la linea sta a 0,45: sopra le sorelle, ben sotto la stessa persona. Cinque foto sono un controllo di buon senso, non una valutazione, e la soglia verrà regolata su veri video di famiglia.
Altre due regole tengono fuori i sosia: in un video un solo frame fortunato non basta (ne servono due a 0,45, o uno nettamente sopra, a 0,57), e quando in un frame ci sono più volti viene considerato solo quello che corrisponde meglio.
Più foto di riferimento
Una foto di riferimento è un'angolazione, una luce e un'età. I bambini cambiano in fretta. Glimt ti permette di aggiungere fino a cinque foto, e il volto in un frame deve somigliare solo a una di esse, così anche un profilo o il viso dello scorso inverno possono essere riconosciuti. Quando in una foto ci sono più persone, tocchi il volto giusto; Glimt non tira mai a indovinare «il più grande».
Scegliere il secondo e inquadrarlo
Ogni frame riceve un punteggio: 35 % per quanto è bello, 20 % per la nitidezza (varianza di un laplaciano su una copia in scala di grigi di 96 × 96: nei frame sfocati i valori cambiano poco da un pixel all'altro), 45 % per la presenza di un volto chiaro. Con le foto di riferimento, tutto questo punteggio vale il 40 % e «è il bambino?» il 60 %: un frame qualsiasi di tuo figlio batte un bellissimo frame di qualcun altro. Il secondo scelto è la finestra con la media migliore, così un buon frame accanto a uno sfocato non vince.
L'inquadratura per un film verticale ha avuto un suo bug che vale la pena raccontare. Ogni frame indicava il volto più chiaro e l'app faceva la media delle posizioni nel secondo. Con due bambini, il volto più chiaro si alternava tra loro e la media finiva sul muro in mezzo. Ora ogni frame indica il bambino (o il volto più in evidenza: dimensione × qualità, più stabile), e l'app prende il gruppo più grande di posizioni vicine tra loro, non la media. Quando il volto è piccolo, il ritaglio zooma finché occupa circa un quinto dell'altezza del frame, al massimo 1,6× per i video.
Quanto è veloce?
La sorpresa: riconoscere un volto è la parte economica, circa 2 ms sul Neural Engine. Decodificare il frame del video costa quindici volte di più. In pratica Glimt passa in rassegna una giornata piena di video in pochi secondi, e un mese in pochi minuti.
In background
Riempire un anno di giorni mancanti richiede un po' di tempo, quindi deve continuare quando esci dall'app. BGContinuedProcessingTask di iOS 26 è fatto apposta: iOS mostra l'avanzamento nella Dynamic Island e nella schermata di blocco. Tre cose che abbiamo imparato a nostre spese, tutte da un piccolo log sul dispositivo:
- Niente GPU in background. Vision e Core ML scelgono la GPU da soli, falliscono in silenzio quando l'app non è in primo piano e ogni frame sembra vuoto. L'accesso alla GPU richiede un entitlement speciale e hardware supportato; l'iPhone 15 Pro non ne segnalava.
- Ma non tutto ha un fallback su CPU. Forzare Vision sulla CPU ha rotto il rilevamento dei volti: la sua rete per la qualità dei volti viene distribuita senza pesi per CPU sull'iPhone («Missing weights path … nonane»). Ora Glimt chiede prima il Neural Engine, poi la CPU, e altrimenti ripiega su semplici riquadri dei volti.
- L'avanzamento deve muoversi. Un giorno con un video lungo da iCloud è rimasto fermo per mezzo minuto e iOS ha terminato il task. Ora Glimt segnala l'avanzamento dopo ogni frame analizzato e ogni passo di download, e chiede a Photos una versione di qualità media di ogni video per l'analisi (i film usano comunque l'originale).
Privacy
Glimt non ha codice di rete. Volti, embedding e scelte restano nello spazio di archiviazione dell'app sull'iPhone. L'unica cosa che può toccare la rete è Photos stessa, quando scarica i tuoi video dal tuo iCloud Photos perché non sono sul telefono.
Crediti: SFace di Yaoyao Zhong e colleghi; conversione ONNX di Chengrui Wang; OpenCV Zoo (Apache 2.0). Domande o idee: glimt@theill.com.