OpenCVだけで顔認証を実装する【YuNet + SFace・C++】〜C#(OpenCvSharp)の現状も実測解説〜

なりすまし判定・ライブネス検知
📌 準備: OpenCV の環境構築がまだの方はこちら → C++ 環境構築ガイドC#(OpenCvSharp)セットアップ

OpenCVだけで顔認証を実装する【YuNet + SFace・C++】〜C#(OpenCvSharp)の現状も実測解説〜

「顔認証を試したい。でも顔認証クラウドAPIは使えない/使いたくない」——実はその用途、OpenCV 本体だけで一通り実装できます。OpenCV 4.5.4 以降には顔検出モデル YuNetcv::FaceDetectorYN)と顔照合モデル SFacecv::FaceRecognizerSF)が標準搭載されており、追加ライブラリなしで「登録画像と目の前の顔が同一人物か」を判定するパイプラインが組めます。

検出(YuNet)→ 整列・切り出し → 特徴量抽出(SFace)→ コサイン類似度で照合

本記事では C++ の動くコードでこの4ステップを最短で実装します。あわせて、C#(OpenCvSharp)では現状どこまでできるのか(結論: 検出は可能・照合は未ラップ)を実測ベースで解説します。


動作環境

  • OpenCV 4.5.4 以降(本記事は 4.10 系で確認。cv::FaceDetectorYN / cv::FaceRecognizerSF は 4.5.4 で追加)
  • C++17 / .NET 8(OpenCvSharp4)
  • コンパイル例: g++ -std=c++17 main.cpp $(pkg-config --cflags --libs opencv4)

環境構築がまだの方は環境構築ガイドを先にどうぞ。

モデルファイルの入手

2つの ONNX モデルを opencv_zoo からダウンロードし、実行ファイルと同じディレクトリに置きます。

モデル ファイル 役割
YuNet face_detection_yunet_2023mar.onnx 顔検出(位置 + 5点ランドマーク)
SFace face_recognition_sface_2021dec.onnx 顔照合(128次元特徴量)

どちらも数MB程度の軽量モデルで、GPU なしの CPU 推論で実用速度が出ます


C++ 実装(全コード)

登録画像 enroll.jpg と照合画像 query.jpg を比較し、同一人物かを判定するコンソールプログラムです。

#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. モデルを読み込む
    auto detector = cv::FaceDetectorYN::create(
        "face_detection_yunet_2023mar.onnx", "", cv::Size(320, 320),
        0.9f,    // スコア閾値
        0.3f,    // NMS 閾値
        5000);   // 上位K件
    auto recognizer = cv::FaceRecognizerSF::create(
        "face_recognition_sface_2021dec.onnx", "");

    // 2. 画像を読み込む
    cv::Mat enroll = cv::imread(argv[1]);
    cv::Mat query  = cv::imread(argv[2]);
    if (enroll.empty() || query.empty()) {
        std::cerr << "画像の読み込みに失敗しました" << std::endl;
        return -1;
    }

    // 3. 顔を検出する(入力サイズは画像ごとに設定し直す)
    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 << "顔が検出できませんでした" << std::endl;
        return -1;
    }

    // 4. 顔を整列・切り出しして特徴量を抽出する
    cv::Mat aligned1, aligned2, feat1, feat2;
    recognizer->alignCrop(enroll, facesEnroll.row(0), aligned1);
    recognizer->feature(aligned1, feat1);
    feat1 = feat1.clone();   // feature() の出力は内部バッファ参照。clone しないと次の呼び出しで上書きされる
    recognizer->alignCrop(query, facesQuery.row(0), aligned2);
    recognizer->feature(aligned2, feat2);
    feat2 = feat2.clone();

    // 5. コサイン類似度で照合する
    double score = recognizer->match(feat1, feat2,
                                     cv::FaceRecognizerSF::DisType::FR_COSINE);
    const double kThreshold = 0.363;   // OpenCV 公式サンプルの推奨閾値
    std::cout << "cosine similarity = " << score << std::endl;
    std::cout << (score >= kThreshold ? "SAME person" : "DIFFERENT person") << std::endl;
    return 0;
}

実行結果

$ ./face_auth enroll.jpg query_same_person.jpg
cosine similarity = 0.6~0.9 程度(同一人物の別写真)
SAME person

$ ./face_auth enroll.jpg query_other_person.jpg
cosine similarity = 0.363 を大きく下回る値
DIFFERENT person

各ステップの解説

ステップ ポイント
FaceDetectorYN::create 第2引数(config)は ONNX では空文字。Size(320,320) は後で setInputSize で上書きするので初期値でよい
setInputSize 画像ごとに必ず呼ぶ。検出対象画像とサイズが合っていないと検出漏れの原因になる
detect 結果は N×15 の cv::Mat(x, y, w, h + ランドマーク5点×2 + スコア)。1行が1つの顔
alignCrop 検出結果のランドマークを使って顔を正規化・切り出し。SFace はこの整列を前提に学習されているため省略不可
feature 128次元の特徴量ベクトルを出力
match FR_COSINE でコサイン類似度。0.363 以上なら同一人物(公式サンプルの閾値)

C#(OpenCvSharp)ではどうか — 検出は可能・照合は未ラップ【実測】

「同じことを OpenCvSharp でやりたい」という C# 開発者は多いはずです。筆者が OpenCvSharp4 4.13.0.20260627(本記事執筆時点の最新) で実測した結論を先に書きます。

機能 C++(OpenCV本体) C#(OpenCvSharp)
顔検出(YuNet) cv::FaceDetectorYN FaceDetectorYN あり(ただし SetInputSize 相当が無い)
顔照合(SFace) cv::FaceRecognizerSF ラップされていないFaceRecognizerSF クラスが存在しない)

OpenCvSharp.Face 名前空間にあるのは Eigen / Fisher / LBPH といった旧世代の顔認識クラスで、SFace は含まれていません。つまり OpenCvSharp だけでは、この記事の照合パイプラインを完結できません(4.13 時点)。

検出(YuNet)までは C# でも動く

using System;
using OpenCvSharp;

class DetectFace
{
    static void Main()
    {
        using var img = Cv2.ImRead("sample.jpg");
        if (img.Empty())
        {
            Console.Error.WriteLine("sample.jpg が読み込めません");
            return;
        }

        // OpenCvSharp には C++ 版の setInputSize が無いため、
        // Create 時の inputSize を画像サイズに合わせて指定する
        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.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);
    }
}

照合まで C# でやりたい場合の選択肢

  1. CvDnn.ReadNetFromOnnx で SFace の ONNX を直接推論する — 可能ですが、alignCrop 相当の顔整列(ランドマークからのアフィン変換)を自前実装する必要があり、ここを省くと精度が大きく落ちます
  2. 照合部分だけ C++ で書いて P/Invoke で呼ぶ — 確実ですが、ネイティブ相互運用の保守コストを抱えます
  3. 照合・判定を含む商用SDKを使う — 顔まわりの処理を C# から完結させたい場合の現実解です

C# 固有の注意として、Matusing で確実に解放すること(特にループ内で作る Mat はリークしやすい)も覚えておいてください。


つまずきポイント

  • setInputSize を忘れる / 使い回す — 検出画像とサイズが一致していないと顔を取りこぼします。画像(フレーム)ごとに設定し直すのが安全です(OpenCvSharp では Create 時の inputSize 指定で代替)
  • feature() の結果を clone しない — 出力 Mat はモデル内部のバッファを参照しているため、次の feature() 呼び出しで静かに上書きされます。複数人の特徴量を保持する場合は必ず clone()
  • alignCrop を飛ばして顔領域をそのまま照合する — SFace は整列済み画像を前提に学習されており、精度が大きく落ちます
  • 閾値 0.363 を絶対視する — 公式サンプルの値であり、照明・カメラ・運用条件によって適切な値は変わります。実運用では自分のデータで誤受入・誤拒否のバランスを確認してください
  • OpenCvSharp で SFace を探し続ける — 4.13 時点でラップされていません。C# で照合まで必要なら上記の選択肢1〜3から選ぶことになります

ここで作った「顔認証」に、まだ足りないもの

このパイプラインは「登録された顔と同じ人物の顔が写っているか」には正しく答えます。ただし、それは「本人がその場にいるか」とは別の問いです。

実は、このコードのカメラの前に本人の顔写真を印刷した紙をかざしても、多くの場合「SAME person」と判定されます。写真には本人の顔特徴がそのまま写っているからで、これはバグではなく、顔照合という仕組みの設計上の性質です。

この問題は実際に手を動かすと一目で分かります → OpenCVで顔認証を実装したら、印刷した写真であっさり突破された【C++】

どんな攻撃パターンがあるかは → なりすまし攻撃(プレゼンテーション攻撃)の種類と、どこまでが現実的な脅威か

🛡 なりすまし対策をお探しの方へ

本ブログを運営するスワローインキュベートは、印刷写真・スマホ画面などによるなりすましを単一フレームから判定するなりすまし判定SDK/API(CPU推論・オフライン動作可)を開発しています。本記事のような顔認証パイプラインに後付けで組み込め、C#/.NET 8 にも正式対応——OpenCvSharp で照合・判定まで完結できない現状の、実用的な選択肢になります。

→ なりすまし判定SDK/APIの詳細を見る
→ API仕様書・サンプルコード


まとめ

  • OpenCV 4.5.4 以降なら YuNet + SFace で顔認証パイプラインが本体だけで組める(C++・CPU で実用速度)
  • 流れは「検出 → 整列 → 特徴量 → コサイン類似度」の4ステップ。setInputSizefeature() の clone が定番の落とし穴
  • C#(OpenCvSharp)は検出(YuNet)まで。SFace は 4.13 時点で未ラップのため、照合まで必要なら DNN 直接推論・P/Invoke・商用SDK のいずれかを選ぶ
  • ただし顔照合は「同じ人物か」を判定する仕組みであり、「本人がその場にいるか」(ライブネス)は別問題。認証システムとして運用するなら、なりすまし対策を必ずセットで検討してください
タイトルとURLをコピーしました