{"id":24,"date":"2026-11-11T08:00:00","date_gmt":"2026-11-10T23:00:00","guid":{"rendered":"https:\/\/tech.swallow-incubate.com\/en\/?p=24"},"modified":"2026-11-09T21:17:59","modified_gmt":"2026-11-09T12:17:59","slug":"face-recognition-yunet-sface","status":"publish","type":"post","link":"https:\/\/tech.swallow-incubate.com\/en\/liveness\/face-recognition-yunet-sface\/","title":{"rendered":"Face Recognition with OpenCV Only: YuNet + SFace in C++ (and Where OpenCvSharp Stands)"},"content":{"rendered":"<p>You want to try face recognition, but you can&#8217;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: <code>cv::FaceDetectorYN<\/code> for face detection with <strong>YuNet<\/strong> and <code>cv::FaceRecognizerSF<\/code> for face recognition with <strong>SFace<\/strong>. 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?<\/p>\n<pre><code class=\"language-text\">Detect (YuNet) -&gt; Align and crop -&gt; Extract features (SFace) -&gt; Match by cosine similarity\n<\/code><\/pre>\n<p>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.<\/p>\n<hr \/>\n<h2>Environment<\/h2>\n<ul>\n<li>OpenCV 4.5.4 or later (tested with 4.10; <code>cv::FaceDetectorYN<\/code> and <code>cv::FaceRecognizerSF<\/code> were added in 4.5.4. The 2023mar YuNet model used below requires OpenCV 4.8 or later)<\/li>\n<li>C++17 \/ .NET 8 (OpenCvSharp4)<\/li>\n<li>Build example: <code>g++ -std=c++17 main.cpp $(pkg-config --cflags --libs opencv4)<\/code><\/li>\n<\/ul>\n<h2>Getting the models<\/h2>\n<p>Download the two ONNX models from the <a rel=\"noopener\" target=\"_blank\" href=\"https:\/\/github.com\/opencv\/opencv_zoo\">OpenCV Zoo<\/a> and put them in the same directory as your executable.<\/p>\n<table>\n<thead>\n<tr>\n<th>Model<\/th>\n<th>File<\/th>\n<th>Role<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>YuNet<\/td>\n<td><code>face_detection_yunet_2023mar.onnx<\/code><\/td>\n<td>Face detection (bounding box + 5 landmarks)<\/td>\n<\/tr>\n<tr>\n<td>SFace<\/td>\n<td><code>face_recognition_sface_2021dec.onnx<\/code><\/td>\n<td>Face recognition (128-dimensional feature vector)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>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.<\/p>\n<p>Note: the Zoo now lists <code>face_detection_yunet_2026may.onnx<\/code> (dynamic input shape, for OpenCV 5.x) as its default. For OpenCV 4.x, use <code>face_detection_yunet_2023mar.onnx<\/code> as in this article.<\/p>\n<h2>The C++ implementation (full code)<\/h2>\n<p>A console program that compares an enrollment image with a query image and decides whether they show the same person.<\/p>\n<pre><code class=\"language-cpp\">#include &lt;opencv2\/opencv.hpp&gt;\n#include &lt;iostream&gt;\n\nint main(int argc, char** argv)\n{\n    if (argc &lt; 3) {\n        std::cerr &lt;&lt; &quot;usage: .\/face_auth &lt;enroll.jpg&gt; &lt;query.jpg&gt;&quot; &lt;&lt; std::endl;\n        return -1;\n    }\n\n    \/\/ 1. Load the models\n    auto detector = cv::FaceDetectorYN::create(\n        &quot;face_detection_yunet_2023mar.onnx&quot;, &quot;&quot;, cv::Size(320, 320),\n        0.9f,    \/\/ score threshold\n        0.3f,    \/\/ NMS threshold\n        5000);   \/\/ top K\n    auto recognizer = cv::FaceRecognizerSF::create(\n        &quot;face_recognition_sface_2021dec.onnx&quot;, &quot;&quot;);\n\n    \/\/ 2. Load the images\n    cv::Mat enroll = cv::imread(argv[1]);\n    cv::Mat query  = cv::imread(argv[2]);\n    if (enroll.empty() || query.empty()) {\n        std::cerr &lt;&lt; &quot;Failed to load an image&quot; &lt;&lt; std::endl;\n        return -1;\n    }\n\n    \/\/ 3. Detect faces (set the input size again for every image)\n    cv::Mat facesEnroll, facesQuery;\n    detector-&gt;setInputSize(enroll.size());\n    detector-&gt;detect(enroll, facesEnroll);\n    detector-&gt;setInputSize(query.size());\n    detector-&gt;detect(query, facesQuery);\n    if (facesEnroll.rows &lt; 1 || facesQuery.rows &lt; 1) {\n        std::cerr &lt;&lt; &quot;No face detected&quot; &lt;&lt; std::endl;\n        return -1;\n    }\n\n    \/\/ 4. Align and crop each face, then extract features\n    cv::Mat aligned1, aligned2, feat1, feat2;\n    recognizer-&gt;alignCrop(enroll, facesEnroll.row(0), aligned1);\n    recognizer-&gt;feature(aligned1, feat1);\n    feat1 = feat1.clone();   \/\/ feature() returns a view of an internal buffer; without clone() the next call overwrites it\n    recognizer-&gt;alignCrop(query, facesQuery.row(0), aligned2);\n    recognizer-&gt;feature(aligned2, feat2);\n    feat2 = feat2.clone();\n\n    \/\/ 5. Match by cosine similarity\n    double score = recognizer-&gt;match(feat1, feat2,\n                                     cv::FaceRecognizerSF::DisType::FR_COSINE);\n    const double kThreshold = 0.363;   \/\/ threshold used in the official OpenCV sample\n    std::cout &lt;&lt; &quot;cosine similarity = &quot; &lt;&lt; score &lt;&lt; std::endl;\n    std::cout &lt;&lt; (score &gt;= kThreshold ? &quot;SAME person&quot; : &quot;DIFFERENT person&quot;) &lt;&lt; std::endl;\n    return 0;\n}\n<\/code><\/pre>\n<h3>Output<\/h3>\n<pre><code class=\"language-text\">$ .\/face_auth enroll.jpg query_same_person.jpg\ncosine similarity = roughly 0.6 to 0.9 (a different photo of the same person)\nSAME person\n\n$ .\/face_auth enroll.jpg query_other_person.jpg\ncosine similarity = well below 0.363\nDIFFERENT person\n<\/code><\/pre>\n<h2>What each step does<\/h2>\n<table>\n<thead>\n<tr>\n<th>Step<\/th>\n<th>Notes<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>FaceDetectorYN::create<\/code><\/td>\n<td>The second argument (config) is an empty string for ONNX models. <code>Size(320,320)<\/code> is only an initial value, because <code>setInputSize<\/code> overrides it later<\/td>\n<\/tr>\n<tr>\n<td><code>setInputSize<\/code><\/td>\n<td>Call it for every image whose size differs from the previous one. If the input size doesn&#8217;t match, <code>detect<\/code> throws <code>cv::Exception<\/code>: &#8220;Size does not match. Call setInputSize(size) if input size does not match the preset size&#8221;<\/td>\n<\/tr>\n<tr>\n<td><code>detect<\/code><\/td>\n<td>Returns an N x 15 <code>cv::Mat<\/code> (x, y, w, h, then 5 landmarks x 2 coordinates, then the score). One row per face<\/td>\n<\/tr>\n<tr>\n<td><code>alignCrop<\/code><\/td>\n<td>Uses the landmarks to normalize and crop the face. SFace was trained on aligned faces, so this step is not optional<\/td>\n<\/tr>\n<tr>\n<td><code>feature<\/code><\/td>\n<td>Outputs a 128-dimensional feature vector<\/td>\n<\/tr>\n<tr>\n<td><code>match<\/code><\/td>\n<td>With <code>FR_COSINE<\/code>, returns cosine similarity. 0.363 or higher means the same person (the threshold used in the official sample)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>What about C# (OpenCvSharp)? It depends on which package family you use<\/h2>\n<p>Many C# developers will want to do the same thing with OpenCvSharp. One thing to know first: OpenCvSharp now ships as two package families. <strong>OpenCvSharp4<\/strong> (4.13.x, wraps OpenCV 4.13, the maintenance line) and <strong>OpenCvSharp5<\/strong> (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.<\/p>\n<table>\n<thead>\n<tr>\n<th>Feature<\/th>\n<th>C++ (OpenCV)<\/th>\n<th>C# OpenCvSharp4 (4.13)<\/th>\n<th>C# OpenCvSharp5 (5.0)<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Face detection (YuNet)<\/td>\n<td><code>cv::FaceDetectorYN<\/code><\/td>\n<td><code>FaceDetectorYN<\/code> available (no <code>SetInputSize<\/code>; pass the size to <code>Create<\/code>)<\/td>\n<td><code>FaceDetectorYN<\/code> with <code>SetInputSize<\/code><\/td>\n<\/tr>\n<tr>\n<td>Face recognition (SFace)<\/td>\n<td><code>cv::FaceRecognizerSF<\/code><\/td>\n<td>Not wrapped (there is no <code>FaceRecognizerSF<\/code> class)<\/td>\n<td><code>FaceRecognizerSF<\/code> with <code>AlignCrop<\/code> \/ <code>Feature<\/code> \/ <code>Match<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The <code>OpenCvSharp.Face<\/code> namespace in OpenCvSharp4 contains the older Eigen, Fisher, and LBPH recognizers, but not SFace. So with <strong>OpenCvSharp4 alone you cannot complete the recognition pipeline in this article<\/strong>. If you can move to .NET 8 and OpenCV 5, OpenCvSharp5 exposes <code>FaceRecognizerSF<\/code>, and the C++ flow above maps onto <code>AlignCrop<\/code>, <code>Feature<\/code> and <code>Match<\/code>. We have not run the OpenCvSharp5 path end to end ourselves, so treat it as &#8220;available&#8221;, not &#8220;verified by us&#8221;.<\/p>\n<h3>Detection (YuNet) in OpenCvSharp4<\/h3>\n<pre><code class=\"language-csharp\">using System;\nusing OpenCvSharp;\n\nclass DetectFace\n{\n    static void Main()\n    {\n        using var img = Cv2.ImRead(&quot;sample.jpg&quot;);\n        if (img.Empty())\n        {\n            Console.Error.WriteLine(&quot;Cannot load sample.jpg&quot;);\n            return;\n        }\n\n        \/\/ OpenCvSharp has no equivalent of the C++ setInputSize,\n        \/\/ so pass the image size as inputSize when creating the detector\n        using var detector = FaceDetectorYN.Create(\n            &quot;face_detection_yunet_2023mar.onnx&quot;, &quot;&quot;,\n            new Size(img.Width, img.Height), 0.9f, 0.3f, 5000);\n\n        using var faces = new Mat();\n        detector.Detect(img, faces);\n        Console.WriteLine($&quot;Faces detected: {faces.Rows}&quot;);\n\n        for (int i = 0; i &lt; faces.Rows; i++)\n        {\n            var box = new Rect((int)faces.At&lt;float&gt;(i, 0), (int)faces.At&lt;float&gt;(i, 1),\n                               (int)faces.At&lt;float&gt;(i, 2), (int)faces.At&lt;float&gt;(i, 3));\n            Cv2.Rectangle(img, box, Scalar.LimeGreen, 2);\n        }\n        Cv2.ImWrite(&quot;result.jpg&quot;, img);\n    }\n}\n<\/code><\/pre>\n<h3>Options if you need recognition in C#<\/h3>\n<ol>\n<li><strong>Use OpenCvSharp5.<\/strong> If .NET 8 and OpenCV 5 are acceptable for your project, this is the shortest path: <code>FaceRecognizerSF<\/code> is wrapped, and the code is a direct port of the C++ above.<\/li>\n<li><strong>Run the SFace ONNX model directly with <code>CvDnn.ReadNetFromOnnx<\/code>.<\/strong> This works, but you have to implement the equivalent of <code>alignCrop<\/code> yourself (an affine transform from the landmarks). Skip it and accuracy drops sharply.<\/li>\n<li><strong>Write only the matching part in C++ and call it through P\/Invoke.<\/strong> Reliable, but you take on the maintenance cost of native interop.<\/li>\n<li><strong>Call a face processing service over HTTPS, if sending images off-device is acceptable in your case.<\/strong> That is not everyone&#8217;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.<\/li>\n<\/ol>\n<p>One C#-specific note: always dispose of <code>Mat<\/code> objects with <code>using<\/code>. A <code>Mat<\/code> created inside a loop is especially easy to leak.<\/p>\n<h2>Common pitfalls<\/h2>\n<ul>\n<li><strong>Forgetting <code>setInputSize<\/code>, or reusing it across images of different sizes.<\/strong> OpenCV checks the input size on every <code>detect<\/code> call and throws <code>cv::Exception<\/code> with the message &#8220;Size does not match. Call setInputSize(size) if input size does not match the preset size&#8221;. Set it again whenever the frame size changes (in OpenCvSharp4, pass the input size to <code>Create<\/code>; OpenCvSharp5 has <code>SetInputSize<\/code>).<\/li>\n<li><strong>Not cloning the result of <code>feature()<\/code>.<\/strong> The output <code>Mat<\/code> refers to an internal buffer of the model, and the next <code>feature()<\/code> call silently overwrites it. If you keep features for several people, always <code>clone()<\/code>.<\/li>\n<li><strong>Skipping <code>alignCrop<\/code> and matching the raw face region.<\/strong> SFace assumes aligned input, and accuracy drops sharply without it.<\/li>\n<li><strong>Treating 0.363 as absolute.<\/strong> 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.<\/li>\n<li><strong>Searching OpenCvSharp4 for SFace.<\/strong> 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.<\/li>\n<\/ul>\n<h2>What this &#8220;face recognition&#8221; is still missing<\/h2>\n<p>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?<\/p>\n<p>In fact, if you hold up a printed photo of the enrolled person in front of the camera, this code will usually say <strong>SAME person<\/strong>. The photo carries the person&#8217;s facial features, so matching succeeds. This is not a bug. It is how face matching works by design.<\/p>\n<ul>\n<li>See it for yourself in a few minutes: <a href=\"https:\/\/tech.swallow-incubate.com\/en\/liveness\/face-recognition-photo-attack\/\">We Built Face Recognition with OpenCV \u2014 and a Printed Photo Got Right Through (C++)<\/a><\/li>\n<li>Which attacks are realistic threats: <a href=\"https:\/\/tech.swallow-incubate.com\/en\/liveness\/presentation-attack-types\/\">Types of Presentation Attacks on Face Recognition \u2014 and Which Ones Are Realistic Threats<\/a><\/li>\n<li>What to decide before using face recognition in production: <a href=\"https:\/\/tech.swallow-incubate.com\/en\/liveness\/anti-spoofing-checklist\/\">Face Anti-Spoofing Design Checklist: 12 Items to Review Before Production<\/a><\/li>\n<\/ul>\n<h2>Summary<\/h2>\n<ul>\n<li>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.<\/li>\n<li>The flow is four steps: detect, align, extract features, compare by cosine similarity. The classic traps are <code>setInputSize<\/code> and forgetting to <code>clone()<\/code> the output of <code>feature()<\/code>.<\/li>\n<li>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.<\/li>\n<li>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.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Build face recognition with OpenCV alone: YuNet detection + SFace matching in C++, with full code, common pitfalls, and what OpenCvSharp4 and OpenCvSharp5 can do in C#.<\/p>\n","protected":false},"author":1,"featured_media":28,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-24","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-liveness"],"_links":{"self":[{"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/posts\/24","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/comments?post=24"}],"version-history":[{"count":1,"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/posts\/24\/revisions"}],"predecessor-version":[{"id":25,"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/posts\/24\/revisions\/25"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/media\/28"}],"wp:attachment":[{"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/media?parent=24"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/categories?post=24"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tech.swallow-incubate.com\/en\/wp-json\/wp\/v2\/tags?post=24"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}