Face Recognition with OpenCV Only: YuNet + SFace in C++ (and Where OpenCvSharp Stands)

Written by

in

·

YuNet + SFace: face recognition with OpenCV only

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::FaceDetectorYN and cv::FaceRecognizerSF were 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#

  1. Use OpenCvSharp5. If .NET 8 and OpenCV 5 are acceptable for your project, this is the shortest path: FaceRecognizerSF is wrapped, and the code is a direct port of the C++ above.
  2. Run the SFace ONNX model directly with CvDnn.ReadNetFromOnnx. This works, but you have to implement the equivalent of alignCrop yourself (an affine transform from the landmarks). Skip it and accuracy drops sharply.
  3. Write only the matching part in C++ and call it through P/Invoke. Reliable, but you take on the maintenance cost of native interop.
  4. 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 every detect call and throws cv::Exception with 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 to Create; OpenCvSharp5 has SetInputSize).
  • Not cloning the result of feature(). The output Mat refers to an internal buffer of the model, and the next feature() call silently overwrites it. If you keep features for several people, always clone().
  • Skipping alignCrop and 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.

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 setInputSize and forgetting to clone() the output of feature().
  • 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).

Try free Plans & pricing API docs