OpenCVで顔認証を実装したら、印刷した写真であっさり突破された【C++】
前回の記事で、OpenCV 標準の YuNet + SFace だけを使って顔認証パイプラインを組みました。登録した本人は通り、他人は弾かれる。ここまでは期待どおりに動きます。
では、カメラの前に立つのが本人ではなく、本人の顔写真を印刷した紙だったらどうなるでしょうか。
結論から書くと——通ります。コンビニのマルチコピー機で印刷した写真で、コサイン類似度は本人判定の閾値を超えてきます。スマホの画面に顔写真を表示してかざしても同じです。
筆者ら(スワローインキュベート)はなりすまし判定SDKの開発・評価の過程でこの挙動を繰り返し実測してきましたが、本記事のコードは手元でそのまま再現できるので、ぜひ自分の顔でご確認ください。
実験の構成
登録画像から特徴量を作っておき、カメラに映った顔と照合し続けるプログラムを用意します。
| ケース | カメラの前にあるもの |
|---|---|
| ① | 本人(実物) |
| ② | 本人の顔写真を印刷した紙 |
| ③ | 本人の顔写真を表示したスマホ画面 |
コード(C++)
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
auto detector = cv::FaceDetectorYN::create(
"face_detection_yunet_2023mar.onnx", "", cv::Size(320, 320));
auto recognizer = cv::FaceRecognizerSF::create(
"face_recognition_sface_2021dec.onnx", "");
// 登録画像(本人の正面写真)から特徴量を作っておく
cv::Mat enroll = cv::imread("enroll.jpg");
if (enroll.empty()) {
std::cerr << "enroll.jpg が読み込めません" << std::endl;
return -1;
}
detector->setInputSize(enroll.size());
cv::Mat facesEnroll;
detector->detect(enroll, facesEnroll);
if (facesEnroll.rows < 1) {
std::cerr << "登録画像から顔が検出できません" << std::endl;
return -1;
}
cv::Mat alignedE, featEnroll;
recognizer->alignCrop(enroll, facesEnroll.row(0), alignedE);
recognizer->feature(alignedE, featEnroll);
featEnroll = featEnroll.clone();
// カメラの前の「何か」を照合し続ける
cv::VideoCapture cap(0);
cv::Mat frame;
while (cap.read(frame)) {
detector->setInputSize(frame.size());
cv::Mat faces;
detector->detect(frame, faces);
if (faces.rows > 0) {
cv::Mat aligned, feat;
recognizer->alignCrop(frame, faces.row(0), aligned);
recognizer->feature(aligned, feat);
double score = recognizer->match(featEnroll, feat,
cv::FaceRecognizerSF::DisType::FR_COSINE);
bool same = score >= 0.363;
cv::Rect box(cv::Point((int)faces.at<float>(0, 0), (int)faces.at<float>(0, 1)),
cv::Size((int)faces.at<float>(0, 2), (int)faces.at<float>(0, 3)));
cv::Scalar color = same ? cv::Scalar(0, 255, 0) : cv::Scalar(0, 0, 255);
cv::rectangle(frame, box, color, 2);
cv::putText(frame, same ? "MATCH" : "NO MATCH", cv::Point(20, 40),
cv::FONT_HERSHEY_SIMPLEX, 1.0, color, 2);
}
cv::imshow("face auth", frame);
if (cv::waitKey(1) == 'q') break;
}
return 0;
}
結果
| ケース | 判定 |
|---|---|
| ① 本人(実物) | MATCH(期待どおり) |
| ② 印刷した顔写真 | MATCH — 閾値 0.363 を超える |
| ③ スマホ画面の顔写真 | MATCH — 同上 |
②③とも、紙やスマホを顔の位置にかざすだけで緑の枠(MATCH)が出ます。印刷のモアレやスマホ画面の反射があっても、YuNet は問題なく顔を検出し、SFace は本人の特徴量を抽出します。
なぜ通ってしまうのか — バグではなく「答えている問いが違う」
種明かしをすると、これは実装ミスでもモデルの欠陥でもありません。
顔照合(Face Recognition)が答えている問いは「この顔は登録された人物と同じか」です。印刷した写真にもスマホ画面にも、本人の顔特徴はそのまま写っています。だから SFace は正しく「同じ人物の顔だ」と答えている——仕組みとして正しく動いた結果、認証としては突破されているわけです。
「本人がその場に実在しているか」は、顔照合とはまったく別の問いです。この問いに答える技術は ライブネス検知(Liveness Detection)/ PAD(Presentation Attack Detection・提示攻撃検知) と呼ばれ、専用の判定器が必要になります。
顔照合: 「誰の顔か」 → 写真でも本人なら一致する(正しい動作)
ライブネス検知: 「生体か偽物か」 → 写真・画面・マスクを見分ける(別の技術)
🛡 この「別の技術」を後付けできます
スワローインキュベートのなりすまし判定SDK/APIは、単一フレームの画像から生体顔かフェイク顔(写真・画面等)かを判定します。C++/C# 対応・CPU推論・オフライン動作可。本記事のようなOpenCV顔認証パイプラインにそのまま組み込めます。
これは現実の脅威なのか
「わざわざ写真を印刷して認証を突破する人がいるのか?」と思うかもしれません。ですが考えてみてください。攻撃素材(顔写真)は SNS やウェブ上に本人が自分で公開していることが珍しくありません。必要な道具はプリンタかスマホだけ。つまり、印刷写真・画面表示によるなりすましは、もっとも低コストで実行できてしまう攻撃です。
だからこそ、勤怠打刻・入退室・本人確認など「なりすまされると誰かが得をする」システムに素の顔認証を置くのは危険です。攻撃にどんな類型があり、用途ごとにどこまで守るべきかは、次の記事で整理しています。
→ なりすまし攻撃(プレゼンテーション攻撃)の種類と、どこまでが現実的な脅威か
まとめ
- OpenCV(YuNet + SFace)の顔認証は、本人の印刷写真・スマホ画面表示で突破できてしまう。本記事のコードで誰でも再現可能
- これはバグではない。顔照合は「誰の顔か」に答える仕組みであり、「本人がその場にいるか」には最初から答えていない
- 認証として運用するなら、顔照合とは別にライブネス検知(PAD)を組み込む必要がある
- 攻撃素材(顔写真)は公開情報から手に入る時代。印刷写真・画面表示は最も低コストな攻撃として、真っ先に対策すべき対象

