Glimt maakt van één seconde per dag een film. Het interessante is dat je die seconde niet zelf hoeft te filmen: Glimt vindt hem in de video's die je al hebt gemaakt, en kiest de seconde waarin jouw kind in beeld is, zelfs in een drukke verjaardagsvideo met vijf andere kinderen. Alles draait op de iPhone: geen server, geen account, geen upload. Zo werkt het, met de modellen, de wiskunde en echte metingen.
De pipeline
Waarom niet gewoon Apple's Vision?
Vision doet hier al veel: het vindt gezichten (VNDetectFaceCaptureQualityRequest, dat ook beoordeelt hoe scherp en frontaal elk gezicht is), hun landmarks (VNDetectFaceLandmarksRequest) en, op iOS 18, hoe mooi een frame is (CalculateImageAestheticsScoresRequest, dat ook kassabonnen en screenshots markeert). Wat het niet biedt, is wie een gezicht is. Het album Personen in Photos weet het, maar die index is niet toegankelijk voor apps.
De eerste versie van Glimt gebruikte VNGenerateImageFeaturePrintRequest op een uitsnede van het gezicht. Dat is een algemene vingerafdruk van een afbeelding: hij vergelijkt haar, licht, kleuren en achtergrond net zo goed als het gezicht zelf. In de praktijk zag hij broers en zussen vrolijk voor elkaar aan, en elk kind dat in dezelfde kamer op hetzelfde moment van de dag was gefotografeerd. Het was het verkeerde gereedschap.
Het gezichtsherkenningsmodel
Glimt heeft nu een eigen model: SFace (Zhong et al., 2021, ‘Sigmoid-Constrained Hypersphere Loss for Robust Face Recognition’), met een MobileFaceNet-backbone, gepubliceerd in de OpenCV Zoo onder de Apache 2.0-licentie. Het haalt 99,40 % op LFW. Het neemt een gezicht van 112 × 112 en geeft 128 getallen terug, een embedding; twee foto's van dezelfde persoon leveren embeddings op die dezelfde kant op wijzen.
De conversie ging van ONNX → PyTorch (onnx2torch) → Core ML (coremltools, ML Program, float16), gevolgd door 8-bit lineaire kwantisatie van de gewichten. Het resultaat is 9,8 MB, laadt de eerste keer in zo'n 0,5 s, en de uitvoer komt met een cosinus van 0,998 overeen met het originele ONNX-model op een testinvoer. De normalisatie van de invoer zit in de graaf, dus de app geeft gewoon RGB-pixels van 0–255 mee.
Uitlijning doet er meer toe dan je denkt
SFace is getraind op gezichten die zijn uitgelijnd op een vast sjabloon: ogen, neuspunt en mondhoeken op bekende plekken in het vierkant van 112 × 112. Een gezicht dat scheef of uit het midden staat, geeft een merkbaar slechtere embedding. De landmarks van Vision leveren de vijf punten (de pupillen, het midden van de neusregio, de buitenste punten van de lippen); Glimt zoekt dan de rotatie, uniforme schaal en verschuiving die ze het best op het sjabloon leggen. In twee dimensies heeft dat kleinstekwadratenprobleem een gesloten oplossing als je de punten als complexe getallen behandelt:
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 past hem toe (één affiene transformatie, software-renderer, een uitvoer van 112 pixels) en de uitsnede gaat naar het model. Referentiefoto's worden twee keer door het model gehaald, zoals ze zijn en gespiegeld, en de twee worden gemiddeld: een iets stabielere referentie, gratis.
Is het hetzelfde kind? De drempel kalibreren
De app vergelijkt de embedding van elk gezicht met de referentiefoto's via cosinusgelijkenis en houdt de beste match. Waar de grens ligt, is gemeten en niet gegokt, op portretten uit het publieke domein van het Witte Huis (officiële portretten met jaren ertussen, en een familieportret met beide ouders en twee dochters):
Dezelfde man op portretten met vier jaar ertussen scoort 0,83; dezelfde mensen uit de familiefoto gelicht 0,70–0,85. Vreemden blijven onder 0,25. Het lastige geval is familie: twee zussen op dezelfde foto scoren 0,43 ten opzichte van elkaar, moeder en dochters tot 0,41 (hetzelfde licht en dezelfde pose laten familieleden meer op elkaar lijken), terwijl moeder tegenover haar dochters op een andere foto zakt naar 0,26–0,30. Dus ligt de grens op 0,45: boven de zussen, ruim onder dezelfde persoon. Vijf foto's zijn een sanity check, geen evaluatie, en de grens wordt nog afgesteld op echte familievideo's.
Nog twee regels houden lookalikes buiten de deur: in een video is één gelukkig frame niet genoeg (er zijn twee frames op 0,45 nodig, of één duidelijk erboven, op 0,57), en als er meerdere gezichten in een frame staan, telt alleen het gezicht dat het best overeenkomt.
Meerdere referentiefoto's
Eén referentiefoto is één hoek, één licht en één leeftijd. Kinderen veranderen snel. Met Glimt kun je tot vijf foto's toevoegen, en het gezicht in een frame hoeft maar op één ervan te lijken, zodat ook een profielfoto of het gezicht van afgelopen winter nog herkend wordt. Staan er meerdere mensen op een foto, dan tik je het juiste gezicht aan; Glimt gokt nooit ‘de grootste’.
De seconde kiezen, en inkaderen
Elk frame krijgt een score: 35 % hoe mooi het is, 20 % scherpte (variantie van een Laplaciaan op een grijze kopie van 96 × 96: in wazige frames veranderen de waarden weinig van pixel tot pixel), 45 % of er een duidelijk gezicht in staat. Met referentiefoto's telt die hele score voor 40 % en ‘is het het kind?’ voor 60 %: een gewoon frame van je kind wint van een prachtig frame van iemand anders. De gekozen seconde is het venster met het beste gemiddelde, zodat één goed frame naast een wazig frame niet wint.
Het inkaderen voor een staande film had een eigen bug die het delen waard is. Elk frame wees het duidelijkste gezicht aan, en de app middelde de posities over de seconde. Met twee kinderen wisselde het duidelijkste gezicht tussen hen, en het gemiddelde kwam uit op de muur ertussen. Nu wijst elk frame het kind aan (of het meest prominente gezicht: grootte × kwaliteit, wat stabieler is), en neemt de app de grootste groep posities die dicht bij elkaar liggen, niet het gemiddelde. Als het gezicht klein is, zoomt de uitsnede in tot het ongeveer een vijfde van de framehoogte beslaat, bij video maximaal 1,6×.
Hoe snel is het?
De verrassing: een gezicht herkennen is het goedkope deel, zo'n 2 ms op de Neural Engine. Het videoframe decoderen kost vijftien keer zoveel. In de praktijk werkt Glimt een drukke dag aan video's in een paar seconden door, en een maand in een paar minuten.
Op de achtergrond
Een jaar aan ontbrekende dagen opvullen duurt even, dus het moet doorgaan als je de app verlaat. BGContinuedProcessingTask uit iOS 26 is daarvoor gemaakt: iOS toont de voortgang in het Dynamic Island en op het toegangsscherm. Drie dingen die we op de harde manier hebben geleerd, allemaal uit een klein log op het apparaat:
- Geen GPU op de achtergrond. Vision en Core ML kiezen zelf de GPU, falen stilletjes als de app niet op de voorgrond staat, en elk frame lijkt leeg. GPU-toegang vereist een speciaal entitlement en ondersteunde hardware; de iPhone 15 Pro meldde geen van beide.
- Maar niet alles heeft een CPU-fallback. Vision op de CPU dwingen maakte de gezichtsdetectie kapot: het netwerk voor gezichtskwaliteit wordt op de iPhone zonder CPU-gewichten geleverd (‘Missing weights path … nonane’). Glimt vraagt nu eerst om de Neural Engine, dan de CPU, en valt anders terug op gewone gezichtskaders.
- De voortgang moet blijven bewegen. Een dag met een lange video uit iCloud maakte een halve minuut geen voortgang, en iOS beëindigde de taak. Glimt meldt nu voortgang na elk geanalyseerd frame en elke downloadstap, en vraagt Photos om een versie van gemiddelde kwaliteit van elke video voor de analyse (films gebruiken nog steeds het origineel).
Privacy
Glimt heeft geen netwerkcode. Gezichten, embeddings en keuzes blijven in de eigen opslag van de app op de iPhone. Het enige dat het netwerk kan raken, is Photos zelf, dat je eigen video's ophaalt uit je eigen iCloud Photos als ze niet op de telefoon staan.
Met dank aan: SFace van Yaoyao Zhong en collega's; ONNX-conversie door Chengrui Wang; OpenCV Zoo (Apache 2.0). Vragen of ideeën: glimt@theill.com.