You want to try face recognition, but you can’t, or would rather not, send faces to a cloud face recognition API. Good news: OpenCV alone can do the whole job. Since OpenCV 4.5.4, the library has had two face classes built in: cv::FaceDetectorYN for face detection with YuNet and cv::FaceRecognizerSF for face recognition with SFace. The ONNX weights come from the OpenCV Zoo. With no extra libraries, you can build a pipeline that answers one question: is the face in this image the same person as the enrolled face?
Detect (YuNet) -> Align and crop -> Extract features (SFace) -> Match by cosine similarity
This article implements those four steps in C++ with working code. We also checked how far you can get in C# with OpenCvSharp. Short version: detection works in both package families; recognition is wrapped only in OpenCvSharp5.
Environment
- OpenCV 4.5.4 or later (tested with 4.10;
cv::FaceDetectorYNandcv::FaceRecognizerSFwere added in 4.5.4. The 2023mar YuNet model used below requires OpenCV 4.8 or later) - C++17 / .NET 8 (OpenCvSharp4)
- Build example:
g++ -std=c++17 main.cpp $(pkg-config --cflags --libs opencv4)
Getting the models
Download the two ONNX models from the OpenCV Zoo and put them in the same directory as your executable.
| Model | File | Role |
|---|---|---|
| YuNet | face_detection_yunet_2023mar.onnx |
Face detection (bounding box + 5 landmarks) |
| SFace | face_recognition_sface_2021dec.onnx |
Face recognition (128-dimensional feature vector) |
Both are lightweight: the YuNet file is about 230 KB and the SFace file about 37 MB, and both run at practical speed on a CPU with no GPU.
Note: the Zoo now lists face_detection_yunet_2026may.onnx (dynamic input shape, for OpenCV 5.x) as its default. For OpenCV 4.x, use face_detection_yunet_2023mar.onnx as in this article.
The C++ implementation (full code)
A console program that compares an enrollment image with a query image and decides whether they show the same person.
#include <opencv2/opencv.hpp>
#include <iostream>
int main(int argc, char** argv)
{
if (argc < 3) {
std::cerr << "usage: ./face_auth <enroll.jpg> <query.jpg>" << std::endl;
return -1;
}
// 1. Load the models
auto detector = cv::FaceDetectorYN::create(
"face_detection_yunet_2023mar.onnx", "", cv::Size(320, 320),
0.9f, // score threshold
0.3f, // NMS threshold
5000); // top K
auto recognizer = cv::FaceRecognizerSF::create(
"face_recognition_sface_2021dec.onnx", "");
// 2. Load the images
cv::Mat enroll = cv::imread(argv[1]);
cv::Mat query = cv::imread(argv[2]);
if (enroll.empty() || query.empty()) {
std::cerr << "Failed to load an image" << std::endl;
return -1;
}
// 3. Detect faces (set the input size again for every image)
cv::Mat facesEnroll, facesQuery;
detector->setInputSize(enroll.size());
detector->detect(enroll, facesEnroll);
detector->setInputSize(query.size());
detector->detect(query, facesQuery);
if (facesEnroll.rows < 1 || facesQuery.rows < 1) {
std::cerr << "No face detected" << std::endl;
return -1;
}
// 4. Align and crop each face, then extract features
cv::Mat aligned1, aligned2, feat1, feat2;
recognizer->alignCrop(enroll, facesEnroll.row(0), aligned1);
recognizer->feature(aligned1, feat1);
feat1 = feat1.clone(); // feature() returns a view of an internal buffer; without clone() the next call overwrites it
recognizer->alignCrop(query, facesQuery.row(0), aligned2);
recognizer->feature(aligned2, feat2);
feat2 = feat2.clone();
// 5. Match by cosine similarity
double score = recognizer->match(feat1, feat2,
cv::FaceRecognizerSF::DisType::FR_COSINE);
const double kThreshold = 0.363; // threshold used in the official OpenCV sample
std::cout << "cosine similarity = " << score << std::endl;
std::cout << (score >= kThreshold ? "SAME person" : "DIFFERENT person") << std::endl;
return 0;
}
Output
$ ./face_auth enroll.jpg query_same_person.jpg
cosine similarity = roughly 0.6 to 0.9 (a different photo of the same person)
SAME person
$ ./face_auth enroll.jpg query_other_person.jpg
cosine similarity = well below 0.363
DIFFERENT person
What each step does
| Step | Notes |
|---|---|
FaceDetectorYN::create |
The second argument (config) is an empty string for ONNX models. Size(320,320) is only an initial value, because setInputSize overrides it later |
setInputSize |
Call it for every image whose size differs from the previous one. If the input size doesn’t match, detect throws cv::Exception: “Size does not match. Call setInputSize(size) if input size does not match the preset size” |
detect |
Returns an N x 15 cv::Mat (x, y, w, h, then 5 landmarks x 2 coordinates, then the score). One row per face |
alignCrop |
Uses the landmarks to normalize and crop the face. SFace was trained on aligned faces, so this step is not optional |
feature |
Outputs a 128-dimensional feature vector |
match |
With FR_COSINE, returns cosine similarity. 0.363 or higher means the same person (the threshold used in the official sample) |
What about C# (OpenCvSharp)? It depends on which package family you use
Many C# developers will want to do the same thing with OpenCvSharp. One thing to know first: OpenCvSharp now ships as two package families. OpenCvSharp4 (4.13.x, wraps OpenCV 4.13, the maintenance line) and OpenCvSharp5 (5.0.x, wraps OpenCV 5, .NET 8+, the actively developed line). We tested OpenCvSharp4 4.13.0.20260627; the result also holds for 4.13.0.20261107, the newest OpenCvSharp4 release as of this writing. Here is the short version first.
| Feature | C++ (OpenCV) | C# OpenCvSharp4 (4.13) | C# OpenCvSharp5 (5.0) |
|---|---|---|---|
| Face detection (YuNet) | cv::FaceDetectorYN |
FaceDetectorYN available (no SetInputSize; pass the size to Create) |
FaceDetectorYN with SetInputSize |
| Face recognition (SFace) | cv::FaceRecognizerSF |
Not wrapped (there is no FaceRecognizerSF class) |
FaceRecognizerSF with AlignCrop / Feature / Match |
The OpenCvSharp.Face namespace in OpenCvSharp4 contains the older Eigen, Fisher, and LBPH recognizers, but not SFace. So with OpenCvSharp4 alone you cannot complete the recognition pipeline in this article. If you can move to .NET 8 and OpenCV 5, OpenCvSharp5 exposes FaceRecognizerSF, and the C++ flow above maps onto AlignCrop, Feature and Match. We have not run the OpenCvSharp5 path end to end ourselves, so treat it as “available”, not “verified by us”.
Detection (YuNet) in OpenCvSharp4
using System;
using OpenCvSharp;
class DetectFace
{
static void Main()
{
using var img = Cv2.ImRead("sample.jpg");
if (img.Empty())
{
Console.Error.WriteLine("Cannot load sample.jpg");
return;
}
// OpenCvSharp has no equivalent of the C++ setInputSize,
// so pass the image size as inputSize when creating the detector
using var detector = FaceDetectorYN.Create(
"face_detection_yunet_2023mar.onnx", "",
new Size(img.Width, img.Height), 0.9f, 0.3f, 5000);
using var faces = new Mat();
detector.Detect(img, faces);
Console.WriteLine($"Faces detected: {faces.Rows}");
for (int i = 0; i < faces.Rows; i++)
{
var box = new Rect((int)faces.At<float>(i, 0), (int)faces.At<float>(i, 1),
(int)faces.At<float>(i, 2), (int)faces.At<float>(i, 3));
Cv2.Rectangle(img, box, Scalar.LimeGreen, 2);
}
Cv2.ImWrite("result.jpg", img);
}
}
Options if you need recognition in C#
- Use OpenCvSharp5. If .NET 8 and OpenCV 5 are acceptable for your project, this is the shortest path:
FaceRecognizerSFis wrapped, and the code is a direct port of the C++ above. - Run the SFace ONNX model directly with
CvDnn.ReadNetFromOnnx. This works, but you have to implement the equivalent ofalignCropyourself (an affine transform from the landmarks). Skip it and accuracy drops sharply. - Write only the matching part in C++ and call it through P/Invoke. Reliable, but you take on the maintenance cost of native interop.
- Call a face processing service over HTTPS, if sending images off-device is acceptable in your case. That is not everyone’s situation (it is the reason many people reach for OpenCV in the first place), but when it is allowed, posting one image to a REST API keeps the C# side free of native code.
One C#-specific note: always dispose of Mat objects with using. A Mat created inside a loop is especially easy to leak.
Common pitfalls
- Forgetting
setInputSize, or reusing it across images of different sizes. OpenCV checks the input size on everydetectcall and throwscv::Exceptionwith the message “Size does not match. Call setInputSize(size) if input size does not match the preset size”. Set it again whenever the frame size changes (in OpenCvSharp4, pass the input size toCreate; OpenCvSharp5 hasSetInputSize). - Not cloning the result of
feature(). The outputMatrefers to an internal buffer of the model, and the nextfeature()call silently overwrites it. If you keep features for several people, alwaysclone(). - Skipping
alignCropand matching the raw face region. SFace assumes aligned input, and accuracy drops sharply without it. - Treating 0.363 as absolute. It is the value from the official sample. The right threshold depends on lighting, camera, and operating conditions. In production, check the balance of false accepts and false rejects on your own data.
- Searching OpenCvSharp4 for SFace. It is not wrapped in the 4.13 line (checked up to 4.13.0.20261107). If you need recognition in C#, either move to OpenCvSharp5 or pick one of the other options above.
What this “face recognition” is still missing
This pipeline correctly answers one question: does the image show the same person as the enrolled face? That is not the same question as: is that person actually standing in front of the camera?
In fact, if you hold up a printed photo of the enrolled person in front of the camera, this code will usually say SAME person. The photo carries the person’s facial features, so matching succeeds. This is not a bug. It is how face matching works by design.
- See it for yourself in a few minutes: We Built Face Recognition with OpenCV — and a Printed Photo Got Right Through (C++)
- Which attacks are realistic threats: Types of Presentation Attacks on Face Recognition — and Which Ones Are Realistic Threats
- What to decide before using face recognition in production: Face Anti-Spoofing Design Checklist: 12 Items to Review Before Production
Summary
- With OpenCV 4.5.4 or later, YuNet + SFace gives you a complete face recognition pipeline in OpenCV itself, in C++, at practical speed on a CPU.
- The flow is four steps: detect, align, extract features, compare by cosine similarity. The classic traps are
setInputSizeand forgetting toclone()the output offeature(). - In C#, YuNet detection works in both OpenCvSharp4 and OpenCvSharp5, but SFace is wrapped only in OpenCvSharp5 (OpenCV 5 / .NET 8+). On OpenCvSharp4 you need direct DNN inference, P/Invoke, or an external API for recognition.
- Face matching tells you whether it is the same person, not whether that person is physically present (liveness). If you run it as an authentication system, plan for presentation attack detection from the start.
Add liveness detection with one REST API call
Our Liveness Detection API tells your face recognition or eKYC flow whether a live person is in front of the camera — from a single still image. Start with the Free plan (50 calls per month).
