Glimt는 매일의 1초를 모아 한 편의 영상으로 만듭니다. 흥미로운 점은 그 1초를 따로 촬영할 필요가 없다는 것입니다. Glimt는 이미 찍어 둔 동영상에서 그 1초를 찾아내고, 다른 아이 다섯 명이 함께 나오는 북적이는 생일 파티 영상에서도 내 아이가 화면에 나오는 1초를 골라냅니다. 이 모든 처리는 iPhone에서 이루어지며, 서버도 계정도 업로드도 없습니다. 이 글에서는 사용한 모델과 수식, 실제 측정 시간을 곁들여 그 원리를 설명합니다.
파이프라인
그냥 Apple의 Vision을 쓰면 안 될까?
Vision은 여기서 많은 일을 합니다. 얼굴을 찾고(VNDetectFaceCaptureQualityRequest, 각 얼굴이 얼마나 선명하고 정면을 향하는지도 평가합니다), 랜드마크를 찾으며(VNDetectFaceLandmarksRequest), iOS 18부터는 프레임이 얼마나 보기 좋은지도 판단합니다(CalculateImageAestheticsScoresRequest, 영수증이나 스크린샷도 걸러 냅니다). 하지만 그 얼굴이 누구인지는 알려 주지 않습니다. 사진 앱의 '인물' 앨범은 알고 있지만, 그 인덱스는 앱에 공개되어 있지 않습니다.
Glimt의 첫 버전은 얼굴을 잘라낸 이미지에 VNGenerateImageFeaturePrintRequest를 사용했습니다. 이건 범용 이미지 지문이라 얼굴 자체만큼이나 머리카락, 조명, 색감, 배경까지 비교합니다. 실제로 형제자매끼리, 또 같은 방에서 같은 시간대에 찍힌 아이라면 별 망설임 없이 같은 사람으로 판단했습니다. 애초에 맞지 않는 도구였던 셈입니다.
얼굴 인식 모델
지금의 Glimt는 자체 모델을 탑재하고 있습니다. SFace(Zhong et al., 2021, "Sigmoid-Constrained Hypersphere Loss for Robust Face Recognition")로, MobileFaceNet 백본을 사용하며 OpenCV Zoo에 Apache 2.0 라이선스로 공개되어 있습니다. LFW 정확도는 99.40 %로 보고되어 있습니다. 112 × 112 얼굴 이미지를 입력받아 128개의 숫자, 즉 임베딩을 반환합니다. 같은 사람의 사진 두 장은 같은 방향을 가리키는 임베딩을 만듭니다.
변환은 ONNX → PyTorch(onnx2torch) → Core ML(coremltools, ML Program, float16) 순서로 진행했고, 이어서 8비트 선형 가중치 양자화를 적용했습니다. 결과물은 9.8 MB이고, 첫 로드에 약 0.5초가 걸리며, 테스트 입력에서 원본 ONNX 모델과 코사인 유사도 0.998로 출력이 일치합니다. 입력 정규화는 그래프 안에 포함되어 있어서 앱은 0–255 범위의 RGB 픽셀을 그대로 넘기기만 하면 됩니다.
정렬은 생각보다 훨씬 중요하다
SFace는 고정된 템플릿에 맞춰 정렬된 얼굴로 학습되었습니다. 112 × 112 정사각형 안에서 눈, 코끝, 입꼬리가 정해진 위치에 오도록 맞춘 것입니다. 기울어지거나 중심에서 벗어난 얼굴은 눈에 띄게 나쁜 임베딩을 만듭니다. Vision의 랜드마크에서 다섯 점(동공, 코 영역의 중심, 입술의 양 끝)을 얻은 뒤, Glimt는 이 점들을 템플릿에 가장 잘 겹치게 하는 회전, 균일 스케일, 이동을 구합니다. 2차원에서는 점을 복소수로 다루면 이 최소제곱 문제를 닫힌 형태로 풀 수 있습니다.
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로 적용하고(아핀 변환 1회, 소프트웨어 렌더러, 112픽셀 출력), 잘라낸 이미지를 모델에 넘깁니다. 참조 사진은 원본 그대로와 좌우 반전한 것으로 두 번 임베딩한 뒤 평균을 냅니다. 추가 비용 없이 조금 더 안정적인 참조를 얻을 수 있습니다.
같은 아이일까? 임계값 보정
앱은 각 얼굴의 임베딩을 참조 사진들과 코사인 유사도로 비교하고 가장 높은 값을 취합니다. 기준선을 어디에 그을지는 추측하지 않고, 퍼블릭 도메인인 백악관 인물 사진(몇 년 간격을 두고 찍은 공식 초상 사진, 그리고 부모와 두 딸이 함께 찍힌 가족사진)으로 측정했습니다.
4년 간격으로 찍은 같은 남성의 초상 사진은 0.83, 가족사진에서 뽑아낸 같은 사람끼리는 0.70~0.85였습니다. 타인은 0.25 미만에 머뭅니다. 어려운 경우는 가족입니다. 같은 사진 속 두 자매는 서로 0.43, 엄마와 딸들은 최대 0.41(조명과 포즈가 같으면 가족끼리 더 닮아 보입니다)이었지만, 다른 사진 속 엄마와 딸들을 비교하면 0.26~0.30으로 떨어졌습니다. 그래서 기준선은 0.45입니다. 자매보다는 위, 같은 사람보다는 한참 아래입니다. 사진 다섯 장은 평가라기보다 정상 동작 확인에 가깝고, 기준선은 실제 가족 영상으로 조정해 나갈 예정입니다.
닮은 사람을 걸러 내는 규칙이 두 가지 더 있습니다. 동영상에서는 운 좋게 맞은 프레임 하나로는 부족합니다(0.45 이상인 프레임 두 개, 또는 확실히 높은 0.57 이상인 프레임 하나가 필요합니다). 그리고 한 프레임에 얼굴이 여러 개 있으면 가장 잘 일치하는 얼굴 하나만 고려합니다.
여러 장의 참조 사진
참조 사진 한 장은 하나의 각도, 하나의 조명, 하나의 나이일 뿐입니다. 아이들은 빠르게 변합니다. Glimt에서는 사진을 최대 다섯 장까지 추가할 수 있고, 프레임 속 얼굴은 그중 한 장과만 닮으면 되므로 옆모습 사진이나 지난겨울의 얼굴도 인식할 수 있습니다. 사진에 여러 사람이 있으면 올바른 얼굴을 탭해서 고릅니다. Glimt가 '가장 큰 얼굴'을 짐작으로 고르는 일은 없습니다.
1초를 고르고 프레이밍하기
각 프레임에는 점수가 매겨집니다. 보기 좋은 정도 35 %, 선명도 20 %(96 × 96 흑백 사본에 대한 라플라시안의 분산. 흐린 프레임은 픽셀 간 변화가 완만합니다), 뚜렷한 얼굴이 있는지 45 %입니다. 참조 사진이 설정되어 있으면 이 점수 전체가 40 %, '그 아이인가'가 60 %를 차지합니다. 다른 사람이 나온 멋진 프레임보다 내 아이가 나온 평범한 프레임이 이기는 셈입니다. 선택되는 1초는 평균이 가장 높은 구간이므로, 흐린 프레임 옆의 좋은 프레임 하나만으로는 이길 수 없습니다.
세로 영상의 프레이밍에서는 공유할 만한 버그가 하나 있었습니다. 각 프레임이 가장 뚜렷한 얼굴을 지목하고, 앱은 1초 동안의 위치를 평균했습니다. 아이가 두 명이면 가장 뚜렷한 얼굴이 두 아이 사이를 오갔고, 평균은 그 사이의 벽에 떨어졌습니다. 지금은 각 프레임이 그 아이(또는 크기 × 품질로 가장 두드러진 얼굴. 이쪽이 더 안정적입니다)를 지목하고, 앱은 평균 대신 서로 가까이 모여 있는 위치 중 가장 큰 그룹을 택합니다. 얼굴이 작으면 프레임 높이의 약 5분의 1이 될 때까지 확대하며, 동영상은 최대 1.6배까지입니다.
얼마나 빠를까?
의외였던 점은 얼굴 인식이 오히려 가벼운 부분이라는 것입니다. Neural Engine에서 약 2 ms면 됩니다. 동영상 프레임 디코딩에는 그 15배가 듭니다. 실제로 Glimt는 바쁜 하루치 동영상을 몇 초 만에, 한 달 치를 몇 분 만에 처리합니다.
백그라운드에서 실행하기
1년 치 빠진 날들을 채우려면 시간이 좀 걸리니, 앱을 나가도 계속 진행되어야 합니다. iOS 26의 BGContinuedProcessingTask가 바로 이런 용도이며, iOS가 진행 상황을 Dynamic Island와 잠금 화면에 표시해 줍니다. 기기 내 작은 로그를 통해 힘들게 배운 세 가지가 있습니다.
- 백그라운드에서는 GPU를 쓸 수 없다. Vision과 Core ML은 알아서 GPU를 선택하는데, 앱이 전면에 있지 않으면 조용히 실패하고 모든 프레임이 비어 있는 것처럼 보입니다. GPU에 접근하려면 특별한 entitlement와 지원 하드웨어가 필요한데, iPhone 15 Pro는 사용할 수 없다고 보고했습니다.
- 그렇다고 모든 것에 CPU 폴백이 있는 건 아니다. Vision을 CPU로 강제하자 얼굴 검출이 깨졌습니다. 얼굴 품질 네트워크가 iPhone에서는 CPU용 가중치 없이 배포되기 때문입니다("Missing weights path … nonane"). 이제 Glimt는 먼저 Neural Engine을, 그다음 CPU를 요청하고, 그래도 안 되면 단순한 얼굴 박스로 대체합니다.
- 진행률은 계속 움직여야 한다. iCloud에서 긴 동영상을 받아야 했던 어느 날은 30초 동안 진행이 멈췄고, iOS가 작업을 종료해 버렸습니다. 이제 Glimt는 프레임 하나를 분석할 때마다, 다운로드가 한 단계 진행될 때마다 진행률을 보고하며, 분석용으로는 사진 앱에 각 동영상의 중간 화질 버전을 요청합니다(영상을 만들 때는 여전히 원본을 사용합니다).
개인정보 보호
Glimt에는 네트워크 코드가 없습니다. 얼굴, 임베딩, 선택 결과는 iPhone의 앱 전용 저장 공간에만 머뭅니다. 네트워크에 닿을 수 있는 유일한 것은 사진 앱 자체이며, 그마저도 기기에 없는 동영상을 여러분의 iCloud 사진에서 가져올 때뿐입니다.
크레디트: SFace는 Yaoyao Zhong 외, ONNX 변환은 Chengrui Wang, OpenCV Zoo(Apache 2.0). 질문이나 아이디어는 glimt@theill.com으로 보내 주세요.