Hello, everyone.
Face recognition can sound like a big topic, but technically it can be treated as "turn a face image into a numeric vector, then search for nearby vectors."
Today, I tested OpenCV Zoo's SFace by generating face embeddings and checking whether nearest neighbor search can find another image of the same person.
SFace is a face recognition model. It converts a face image into a 128-dimensional embedding. An embedding is a list of numbers that represents image features. The expectation is that faces from the same person will have nearby embeddings.
In this verification, I used two face image datasets and checked the following points:
Cosine similarity measures how close the directions of two vectors are. In this test, I L2-normalized the embeddings first, which means adjusting each vector length to 1 before comparison.
Target lab: kiarina/labs/2026/07/10/sface-face-embedding
You can build the verification environment and run it locally with the commands below. mise
and uv
are required. The first run also needs an internet connection because it downloads the SFace ONNX model, Olivetti Faces, and LFW.
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/kiarina/labs.git
cd labs
git sparse-checkout set .gitignore .mise/tasks Makefile mise.toml 2026/07/10/sface-face-embedding
mise -C 2026/07/10/sface-face-embedding run
LFW uses about 395 MB after extraction. Including the Python environment and model file, it is safer to reserve roughly 500 MB of free space.
The model used here is OpenCV Zoo's face_recognition_sface_2021dec.onnx
.
https://media.githubusercontent.com/media/opencv/opencv_zoo/47534e27c9851bb1128ccc0102f1145e27f23f98/models/face_recognition_sface/face_recognition_sface_2021dec.onnx
0ba9fbfa01b5270c96627c4ef784da859931e02f04419c829e83484087c34e79
The SFace directory in OpenCV Zoo explicitly states that all files in that directory are licensed under the Apache 2.0 License.
I used two datasets for comparison.
Olivetti Faces contains 40 people, 10 images per person, and 400 total 64x64 grayscale face images. Grayscale means black-and-white images.
images: 400
resolution: 64x64 grayscale
people: 40
images per person: 10
dataset SHA-256: b612fb967f2dc77c9c62d3e1266e0c73d5fca46a4b8906c18e454d41af987794
Labeled Faces in the Wild is a face image dataset closer to real-world conditions. In this test, I used the funneled version through sklearn.datasets.fetch_lfw_people
, selected the first 40 people with at least 20 images in a fixed order, and used only the first 10 images for each person. "Funneled" means the faces have been preprocessed so their orientation and position are somewhat aligned.
selected images: 400
selected resolution: 125x94 RGB
selected people: 40
selected images per person: 10
archive SHA-256: b47c8422c8cded889dc5a13418c4bc2abbda121092b3533a83306f90d900100a
For each dataset, I used 40 people x 10 images. For each person, the first 5 images were used as the reference set, and the last 5 images were used as queries.
The flow is:
cv2.FaceRecognizerSF.feature
Top-1 accuracy is the ratio where the nearest image is from the same person. Top-5 accuracy is the ratio where at least one image from the same person appears in the nearest 5 results.
This test does not perform face detection or additional landmark alignment. Landmark alignment is preprocessing that uses points such as eyes and nose to align the face. Here, I simply resized the dataset crops to 112x112.
The results executed on a Mac Studio (Apple M4 Max) are as follows.
Dataset | Top-1 | Top-5 | Pos cosine | Neg cosine | sec/image
-------------------------------------------------------------------------------------
Olivetti Faces | 0.800 | 0.920 | 0.722 | 0.577 | 0.0057
LFW people, funneled color crop | 0.820 | 0.945 | 0.463 | 0.228 | 0.0058
The verification environment was:
machine: Mac Studio (Apple M4 Max)
OS: macOS 26.5.1, arm64
Python: 3.12.10
OpenCV: 5.0.0
NumPy: 2.5.1
SciPy: 1.18.0
scikit-learn: 1.9.0
Pillow: 12.3.0
More detailed results are saved in the lab as output/olivetti_report.json
, output/lfw_report.json
, and output/summary.json
.
The technical data is shown above, but here is the simpler reading.
SFace embeddings grouped same-person images reasonably well
Olivetti Faces reached 80.0% top-1 accuracy and 92.0% top-5 accuracy. Even with small 64x64 grayscale face images, images of the same person were often close in embedding space.
LFW performed slightly better
The funneled color crop version of LFW reached 82.0% top-1 accuracy and 94.5% top-5 accuracy. The condition is closer to what SFace expects: color face crops that are somewhat aligned.
LFW had a clearer gap between same-person and different-person pairs
For Olivetti Faces, the mean cosine similarity was 0.722 for same-person pairs and 0.577 for different-person pairs. The gap was 0.145. For LFW, the mean was 0.463 for same-person pairs and 0.228 for different-person pairs, with a larger gap of 0.235. The absolute values were lower for LFW, but the separation between same-person and different-person pairs was clearer.
This is not complete identification
Even on LFW, 18% of queries had a different person as the top-1 nearest neighbor. This is training-free nearest neighbor search, not a trained classifier. It also does not add face detection or landmark alignment. So this should be read as a technical check under this specific condition, not as a general face recognition benchmark.
Embedding generation was lightweight
Generating embeddings for 400 images took about 2.3 seconds for both datasets. That is about 0.0058 seconds per image. At this scale, it was very manageable even on CPU.
SFace felt quite practical as a component for turning face images into numbers that are easy to compare. It can be called directly through OpenCV's FaceRecognizerSF
, and the lab also fixes the model URL and verifies the SHA-256 hash, which makes the experiment easy to reproduce.
Humans probably do not recognize someone from the face alone either. We also use voice, speaking style, clothing, and the surrounding context to remember who someone might be. Seen that way, if this result is used as one reference signal rather than as a complete face-only identifier, integrating it into an LLM agent feels realistic.