Aayush Goenka

How I Organize Photos by Face with Local AI

8 min read

A small bundle of blank kraft name tags tied with string on a walnut desk beside a stack of face-down photo prints, with one deep-green tag on top.

Summary

  • I wanted to find every photo of a person without handing my family’s faces to an AI company.
  • My Mac now names faces offline, but only when a new face closely matches faces I named myself.
  • The model with the best public test score failed on real family photos, so I switched models and made the matching stricter.

Have you ever tried to find every photo of one person? Your dad at every birthday, or your kid at every age? A search by name only works if something has put names on the faces first.

I wanted that for my own photos. But my photos already sit in iCloud, and I didn’t want to hand my family’s faces to an AI company too. So I organize my photos by face offline, on my Mac, as part of the local AI photo library I built in October. Everything had to run locally, which limited me to models that were free, ran offline, and worked on my machine.

It now knows about 120 people across roughly 1,300 faces I named myself. Getting there had one big surprise: the first face model I picked scored 98.7% on a well-known public test, then couldn’t recognize people I had named many times.

Short answer: Yes, one Mac can name your family’s faces with nothing leaving it. What made it work was checking every model against faces I had named myself: that check took me from 79% to 91% of them recognized, with about 1 wrong name in 100. Here are the specifics: how it works, the models I tested, why I switched, how strict the matching is, and how I name faces in batches.

How offline face recognition works

Face recognition uses two models, programs that learned a skill from a huge pile of examples, plus a person who knows the names:

Three parts

FindThe first model finds each face and draws a box around it.
FingerprintThe second turns each face into a list of numbers that comes out nearly the same for the same person in different pictures.
NameOnly I can say a face belongs to my wife. Every face I name becomes one more example to compare new faces against.

Compare two fingerprints and you get a similarity score between 0 and 1. Close to 1 means probably the same person. Close to 0 means strangers. The match threshold is the score where “same person” begins. Set it too low and strangers get names. Set it too high and people you know go unrecognized.

Choosing models on public photos

I narrowed the choice on public photo sets first. One was LFW (Labeled Faces in the Wild), a free collection of photos of named people that researchers use to test face models. I added cats and dogs too, because our cat, Chandi is in a lot of my photos.

To compare models fairly, I set each one strict enough that only 1 pair of strangers in 100 slipped through as “the same.”

Job Model On public photos
Find faces YuNet A face in all 792 test pictures
Fingerprint faces SFace Matched 98.7% of person pairs
Find pets RT-DETRv2 99.3% of cats, 98.0% of dogs
Fingerprint pets SigLIP2 Matched 89.1% of cat pairs, 65.8% of dog pairs

“Matched” counts real same-person or same-pet pairs that passed the 1-in-100 rule. Pet figures come from the Oxford pets photo set. The pet finder found no pets in 300 photos of people.

YuNet1 is a tiny, fast face finder. SFace2 came from the same free OpenCV collection. The pet fingerprint came from a SigLIP2 animal identification model,3 trained to tell individual cats and dogs apart.

On a Mac, you might ask why I didn’t just use what Apple already includes.

Why not Apple’s built-in tools?

If you only want to find people inside the Photos app, its People & Pets album4 is free and already does this. I needed the names outside Photos, in my own search and captions, and Apple gives other apps no way to read them.5 I also couldn’t measure or tune how strict it is.

I did test Apple Vision, the picture tools built into macOS. It found faces as well as YuNet, but fewer pets. And it has no fingerprint made for telling people apart: its general one matched only about 37% of same-person pairs under the same rule.

Why I replaced SFace with AdaFace

LFW’s pictures show one face, centered in the frame. Family photos don’t. They’re full of group shots, small faces, and turned heads. When I measured SFace on my roughly 1,300 named faces, it couldn’t recognize the same person from the side, in a blurry shot, or years apart.

Compared one pair at a time, two pictures of the same person usually scored about 0.40. Two different people scored as high as 0.36. That’s too close for any threshold to separate. People I had named many times were never recognized again.

So I tested AdaFace,6 a face model built for poor-quality faces. I shrank and blurred LFW’s faces and ran both models under the same 1-in-100 rule:

Same-person pairs SFace AdaFace
Both clear 99.3% 99.8%
One clear, one small and blurry 90.8% 99.4%
Both blurry 84.5% 98.0%

On clear faces, the two were nearly tied. The gap opened on exactly the kind of faces family photos are full of.

Then I compared them on my own named faces, both allowed the same rate of wrong matches. The test reported only counts, never photos, the same way I kept my AI coding assistant away from the photos.

SFace79%of my named faces recognized
AdaFace91%of my named faces recognized
Unnamed faces linked11 vs 149of 992, SFace vs AdaFace

These are the same faces I later used to set the threshold. So they compare the two models; they don’t predict how either will do on next year’s photos.

Fine for my library, not for sharing

AdaFace’s code is free to use under the MIT license. But the weights, the numbers the model learned in training, come with no license, and they learned from a photo set meant for research only. I use them only in my private library. Anything shared or sold would need a different model.

How strict the matching is

I got the threshold wrong the first time. With SFace, the naming page suggested a name whenever a face scored 0.40 against someone, the typical one-pair score above. About half the faces nobody had named, plus some blurry things that weren’t faces, showed up as possibly someone.

The mistake: a new face isn’t compared with one picture. It’s compared with hundreds of named faces, and in a crowd that big, someone usually looks a bit closer than a typical pair does. The closest wrong person usually scored 0.41 and at most 0.55, so I raised SFace’s threshold to 0.55.

With AdaFace, I changed the rule itself. A new face is scored against the 3 named pictures of each person it looks most like, averaged, so one look-alike picture can’t pull a stranger into someone’s name. The scores shift with the model, so the thresholds moved too:

Average score What happens
0.39 or higher Named automatically
0.24 to 0.39 Shown on the naming page with a guessed name
Below 0.24 Left unnamed

Automatic naming also requires the best person to beat the runner-up clearly, so two similar people never get a coin flip.

I tested this on 1,214 faces I had already named, hiding each photo’s own faces so they couldn’t match themselves. At 0.39, 1,107 got the right name, 12 got the wrong name, and 95 stayed unnamed. Down to 0.24, the guesses were right 1,157 times and wrong 24 times, and a wrong guess is easy to fix on the naming page. I picked both thresholds using these same faces, so the results describe them, not new photos.

Only my names count

New faces are compared only with faces I named myself, never with names the system added on its own. In an earlier version, one wrong automatic name pulled in more wrong ones. Now a mistake stays a single mistake.

Naming faces in batches

The first versions of the naming page asked about everything. At one point it showed over 3,000 groups to click through. I changed it nine times in one evening.

The version that stuck shows 48 faces a screen, with look-alikes side by side. Likely matches come first, labeled with the guess. Then come unnamed faces that keep turning up: at least 5 pictures from 3 different photos or videos. I select a batch and answer once:

  1. A name. The faces join that person’s examples.
  2. Stranger. A real face I don’t need to name.
  3. Not a face. A blur or a pattern the finder got wrong.
  4. Skip. Decide later.

Each picture is decided once and never asked about again. A face seen once or twice that looks like nobody (a guest, someone in the background) gets no question and stays unnamed. Before that rule, about 500 of the faces on the page were those one-offs.

Pets got the same treatment at first.

Why Chandi isn’t on the page

An earlier version asked about pets too. Chandi alone produced nearly 700 questions, and the pet fingerprint couldn’t tell her apart from goats, floors, or cushions. The page now leaves pets out, and the system names a pet only when the match is very close.

Each night, new faces above the threshold get a name. The rest wait until I open the page.

What I’d do from the start

If you try this yourself, use public photos to narrow the choice. Then measure again on a few hundred faces you’ve named, because that’s where my top-scoring model fell apart.

My view

Leave unsure faces unnamed; a blank is easier to fix than a wrong name. And be most careful with the names you give by hand. A wrong automatic name files one photo under the wrong person. A wrong name you give becomes an example every later face is checked against.

For technical readers, here are more details

The public tests ran on October 12, 2026. “1 in 100” means the threshold where 1% of stranger pairs were accepted. LFW faces are centered, so group photos and small or turned faces weren’t covered by that test.

Model Job Time per picture
YuNet (2023 version) Find faces About 2 ms, main processor
SFace Fingerprint faces 6 ms, main processor
RT-DETRv2 r18 Find pets 89 ms, main processor
SigLIP2 Fingerprint pets 40 ms, main processor
AdaFace IR-101 Fingerprint faces About 3 ms, graphics chip

The pet finder was RT-DETRv27 (the small r18 version, trained on the COCO photo set). Pet results used 300 cat and 300 dog photos from Oxford-IIIT Pets;8 a find counted at a confidence of 0.5 or more.

AdaFace is the IR-101 network trained on WebFace12M, using the CVLface weights9 and the network code from CVLface.10 Each fingerprint is 512 numbers, and two are compared by multiplying them number by number and adding up (cosine similarity). Fingerprinting 7,885 library faces again took 656 seconds. Faces are aligned to five landmark points before fingerprinting. The blurry LFW test used LFW’s 6,000 standard pairs.

The real-name test used 1,304 faces of 120 people, split into five groups by file; the 1,214 with a match in another group were scored. Each face was matched against the other four groups, never against its own file or anything taken within 2 minutes of it, so bursts couldn’t match themselves. Automatic naming needs the best person at least 0.05 ahead of the next one. Of 3,525 faces of other people in a photo with someone named, 5 were taken for that person at 0.39 and 19 at 0.24. With SFace, the automatic rule used the single closest picture at 0.55, and a closest-3 guess line at 0.45 got 1,030 right and 22 wrong of 1,203.

Pets are linked automatically only at 0.94, using the single closest named picture. Within one video, a face joins an earlier sighting at 0.35 or more, above any two LFW strangers (0.28), so a person counts once per video, not once per frame. The smallest face kept is 40 pixels on a side, a guess that hasn’t been measured.

This is custom code for my own library, not a package you can install. The method carries over; the exact thresholds depend on the model and your photos, so measure your own.

← All essays