QRコードを使った顔認証
dlib の顔認識モデルに日本人の顔データを追加した Taguchi model を使ってQRコードを使った顔認識アプリケーションを作成します。
Taguchi model の概略
dlib には優秀な顔認証モデルがあります。しかし、残念ながら人種に偏りがあり、特に北東アジア人、とりわけ日本人に関してはまったく役に立たないと言っても過言ではありません。ですが、それは仕方のないことだと思います。訓練に使用した顔認証用のデータセットに偏りがあったためです。
そこで私は日本人を中心とした多くのデータセットを収集し、ゼロから訓練を行いました。これには途方もない時間を要しましたが、ある程度は実用に耐えうる基準に達したと思います。日本人用に訓練したとは言っても欧米人の顔認証に関しても dlib のモデルに近い結果となってます。dlib の example で用意されているハリウッドのアクション ヒーローの写真も dlib と同様に分類が可能です。
詳細な説明は下記をご覧ください
ソースコードはたったの3本
MainWindow.xaml
<Window x:Class="FaceRecognitionUsingQRcode.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FaceRecognitionUsingQRcode"
mc:Ignorable="d"
Title="MainWindow" Height="650" Width="600" WindowStartupLocation="CenterScreen">
<Grid>
<StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 20 0 10">
<Button
Name="CaptureStart" Width="100" Margin="0 0 80 10"
HorizontalAlignment="Center" Click="CaptureStart_Click"
Content="Start" FontSize="14"/>
<Button
Name="CaptureStop" Width="100" Margin="0 0 0 10"
HorizontalAlignment="Center" Click="CaptureStop_Click"
Content="Stop" FontSize="14" IsEnabled="False" />
</StackPanel>
<TextBlock x:Name="ResultMessage"
Background="Yellow" TextAlignment="Center"
Margin="0 0 0 10" FontSize="16" Foreground="Red">
QR code Read Result message.
</TextBlock>
<Border Width="460">
<Image
x:Name="ImgBarcode"
Margin="0 0 0 20" Width="400" Height="400"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Source="Images/camera.png"/>
</Border>
<TextBlock
x:Name="txtQrcode" TextAlignment="Center"
Margin="10" Width="300" Height="25" FontSize="14"
Background="SkyBlue"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
/*
Written by Taguchi.
QRコードをWebカメラで読む方法と OpenCvSharp で顔を検出する方法のサンプルコードです。
VisualStudio 2022 でNugetから以下を入手しました。
Nuget:OpenCvSharp4.Windows
Nuget:OpenCvSharp4.Extensions
Nuget:OpenCvSharp4.WpfExtensions
Nuget:ZXing.Net
----------------------------------------------------------
*/
using System.Drawing;
using System.Windows;
using System.Windows.Media.Imaging;
using OpenCvSharp; // Nuget:OpenCvSharp4.Windows
using OpenCvSharp.Extensions; // Nuget:OpenCvSharp4.Extensions
using OpenCvSharp.WpfExtensions; // Nuget:OpenCvSharp4.WpfExtensions
using ZXing.QrCode;
using ZXing; // Nuget:ZXing.Net
using System.IO;
using ZXing.Common;
using System.Drawing.Imaging;
using System.Diagnostics;
namespace FaceRecognitionUsingQRcode
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : System.Windows.Window
{
static string facesFolder = AppDomain.CurrentDomain.BaseDirectory + @"faces\";
static string tempfolder = AppDomain.CurrentDomain.BaseDirectory + @"faces\temp\";
VideoCapture camera = new VideoCapture(0);
string QRcode_read = "";
int rc = 0;
public MainWindow()
{
InitializeComponent();
}
/// <summary>
/// Shooting begins
/// </summary>
private async void CaptureStart_Click(object sender, RoutedEventArgs e)
{
try
{
if (!Directory.Exists(facesFolder))
{
ResultMessage.Text = "faces folder not found!";
ResultMessage.Background = System.Windows.Media.Brushes.Yellow;
return;
}
// Delete files in temporary folder.
if (!Directory.Exists(tempfolder))
{
ResultMessage.Text = "temporary folder not found!";
ResultMessage.Background = System.Windows.Media.Brushes.Yellow;
return;
}
string[] filePaths = Directory.GetFiles(tempfolder);
foreach (string filePath in filePaths)
{
File.SetAttributes(filePath, FileAttributes.Normal);
File.Delete(filePath);
}
ResultMessage.Text = "";
txtQrcode.Text = "";
QRcode_read = "";
rc = 0;
if (camera != null)
{
camera.Dispose(); //Memory release
}
//capture_loop = false;
ResultMessage.Background = System.Windows.Media.Brushes.White;
camera = new VideoCapture(0);
if (!camera.IsOpened())
{
ResultMessage.Text = "Webcam not found!" + Environment.NewLine + "Please check your webcam.";
ResultMessage.Background = System.Windows.Media.Brushes.Yellow;
return;
}
//camera.Set(OpenCvSharp.CaptureProperty.AutoFocus, 0); // 0:Auto focus off
//camera.Set(OpenCvSharp.CaptureProperty.Focus, 10); // 0-250
CaptureStart.IsEnabled = false;
CaptureStop.IsEnabled = true;
await QRcodeCaptureAsync();
camera.Dispose(); //Memory release
if (rc == 0)
{
txtQrcode.Text = "QR code is '" + QRcode_read + "'";
}
else if (rc == -100)
{
ResultMessage.Text = "The missing QR code is '" + QRcode_read + "'";
ResultMessage.Background = System.Windows.Media.Brushes.Yellow;
return;
}
else if (rc == -200)
{
ResultMessage.Text = "It is not a QR code format.";
ResultMessage.Background = System.Windows.Media.Brushes.Yellow;
return;
}
else
{
ResultMessage.Text = "System error!";
ResultMessage.Background = System.Windows.Media.Brushes.Yellow;
return;
}
Thread.Sleep(3000);
camera = new VideoCapture(0);
if (!camera.IsOpened())
{
ResultMessage.Text = "Webcam not found! Please check your webcam.";
ResultMessage.Background = System.Windows.Media.Brushes.Yellow;
return;
}
await FaceCaptureAsync();
camera.Dispose(); //Memory release
this.ImgBarcode.Source = GetCameraImage();
// C++ call
ProcessStartInfo app = new ProcessStartInfo
{
FileName = "FaceRecognition.exe",
CreateNoWindow = true, // console window close
UseShellExecute = false, // Do not use shell functions
Arguments = QRcode_read // QR code
};
Process? p = Process.Start(app);
p.WaitForExit(); // wait for process to finish
int iExitCode = p.ExitCode;
if (iExitCode == 0)
{
string str_accuracy = File.ReadAllText(facesFolder + QRcode_read + @"\accuracy.txt");
float accuracy = float.Parse(str_accuracy);
// If the accuracy is less than 0.6, it can be determined that the person is the real person.
// If higher accuracy is required, it is possible to set an even lower value,
// but please select a value with the understanding that the person in question may be identified as someone else.
ResultMessage.Text = accuracy < 0.6 ? "The system has authenticated you." : "The system could not authenticate you.";
txtQrcode.Text += " / accuracy is " + str_accuracy;
}
else
{
ResultMessage.Text = "An error was detected in the system.";
}
ResultMessage.Background = System.Windows.Media.Brushes.Yellow;
CaptureStop.IsEnabled = false;
CaptureStart.IsEnabled = true;
}
catch (Exception)
{
ResultMessage.Text = "Application Exception!";
ResultMessage.Background = System.Windows.Media.Brushes.Yellow;
}
}
/// <summary>
/// Shooting canceled
/// </summary>
private void CaptureStop_Click(object sender, RoutedEventArgs e)
{
if (camera != null)
{
camera.Dispose(); //Memory release
}
CaptureStop.IsEnabled = false;
CaptureStart.IsEnabled = true;
this.ImgBarcode.Source = GetCameraImage();
ResultMessage.Background = System.Windows.Media.Brushes.White;
ResultMessage.Text = "";
}
/// <summary>
/// QR code Capture
/// </summary>
private async Task QRcodeCaptureAsync()
{
await Task.Run(() =>
{
Boolean loopFlg = true;
ZXing.Result result;
using (Mat img = new Mat())
using (camera)
{
while (loopFlg)
{
camera.Read(img);
if (img.Empty()) { break; }
this.Dispatcher.Invoke(() =>
{
Bitmap bitmap = img.ToBitmap();
MemoryStream memoryStream = new MemoryStream();
bitmap.Save(memoryStream, ImageFormat.Bmp);
byte[] byteArray = memoryStream.GetBuffer();
LuminanceSource source = new RGBLuminanceSource(byteArray, bitmap.Width, bitmap.Height);
var binarizer = new HybridBinarizer(source);
var binBitmap = new BinaryBitmap(binarizer);
QRCodeReader qrCodeReader = new QRCodeReader();
result = qrCodeReader.decode(binBitmap);
if (result == null)
{
this.ImgBarcode.Source = BitmapSourceConverter.ToBitmapSource(img);
}
else
{
bitmap.Dispose();
if (result.BarcodeFormat != ZXing.BarcodeFormat.QR_CODE)
{
string message = "NOT IN QR CODE FORMAT!";
Cv2.PutText(img, message, new OpenCvSharp.Point(10, 240), HersheyFonts.HersheyDuplex, 1.5, Scalar.Red, 2);
this.ImgBarcode.Source = BitmapSourceConverter.ToBitmapSource(img);
rc = -200;
}
else
{
QRcode_read = result.Text;
txtQrcode.Text = result.Text;
string message = "OK! Please face the webcam.";
Scalar color = Scalar.Green;
string filename = facesFolder + QRcode_read + @"\" + QRcode_read + ".png";
if (!File.Exists(filename))
{
rc = -100;
message = "QR code file not found!";
color = Scalar.Red;
}
OpenCvSharp.Rect rectMsg = new OpenCvSharp.Rect(10, 10, 600, 40);
Cv2.Rectangle(img, rectMsg, new OpenCvSharp.Scalar(206, 230, 193), -1);
Cv2.PutText(img, message, new OpenCvSharp.Point(20, 40), HersheyFonts.HersheyDuplex, 1.2, color, 2);
this.ImgBarcode.Source = BitmapSourceConverter.ToBitmapSource(img);
loopFlg = false; // loop stop
}
}
});
}
}
});
}
/// <summary>
/// Face Capture
/// </summary>
private async Task FaceCaptureAsync()
{
await Task.Run(() =>
{
string message1 = "Authenticating,";
string message2 = "please wait a moment.";
Boolean loopFlg = true;
int imgCounter = 0;
DateTime startTime = DateTime.Now;
using (Mat img = new Mat())
using (camera)
{
while (loopFlg)
{
camera.Read(img);
if (img.Empty()) { break; }
this.Dispatcher.Invoke(() =>
{
using (var cascade = new CascadeClassifier(@"haarcascade_frontalface_default.xml"))
{
foreach (OpenCvSharp.Rect rectDetect in cascade.DetectMultiScale(img))
{
DateTime currentTime = DateTime.Now;
TimeSpan elapsed = currentTime - startTime;
if (elapsed.TotalSeconds >= 0.5)
{
Cv2.ImWrite(tempfolder + @"\face_" + imgCounter + ".jpg", img);
startTime = currentTime;
imgCounter++;
if (imgCounter >= 10)
{
loopFlg = false;
}
}
OpenCvSharp.Rect rect = new OpenCvSharp.Rect(rectDetect.X, rectDetect.Y, rectDetect.Width, rectDetect.Height);
Cv2.Rectangle(img, rect, new OpenCvSharp.Scalar(255, 255, 0), 2);
}
OpenCvSharp.Rect rectMsg = new OpenCvSharp.Rect(10, 10, 600, 80);
Cv2.Rectangle(img, rectMsg, new OpenCvSharp.Scalar(206, 230, 193), -1);
Cv2.PutText(img, message1, new OpenCvSharp.Point(20, 40), HersheyFonts.HersheyDuplex, 1.3, Scalar.Green, 2);
Cv2.PutText(img, message2, new OpenCvSharp.Point(40, 80), HersheyFonts.HersheyDuplex, 1.3, Scalar.Green, 2);
this.ImgBarcode.Source = BitmapSourceConverter.ToBitmapSource(img);
}
});
}
}
});
}
/// <summary>
/// Get logo mark
/// </summary>
private BitmapImage GetCameraImage()
{
BitmapImage bmpImage = new BitmapImage();
using (FileStream stream = File.OpenRead(@"Images/camera.png"))
{
bmpImage.BeginInit();
bmpImage.StreamSource = stream;
bmpImage.DecodePixelWidth = 500;
bmpImage.CacheOption = BitmapCacheOption.OnLoad;
bmpImage.CreateOptions = BitmapCreateOptions.None;
bmpImage.EndInit();
bmpImage.Freeze();
}
return bmpImage;
}
}
}
FaceRecognition.cpp
/*
Written by Taguchi.
QRコードを利用した顔認証のサンプルコードです。
「taguchi_face_recognition_resnet_model_v1.dat」と
「shape_predictor_5_face_landmarks.dat」の
詳細については以下を参照してください。
https://github.com/TaguchiModels/dlibModels
単体テスト用のコマンド例) ./FaceRecognition QRcode
*/
#include <fstream>
#include <dlib/dnn.h>
#include <dlib/gui_widgets.h>
#include <dlib/clustering.h>
#include <dlib/string.h>
#include <dlib/image_io.h>
#include <dlib/image_processing/frontal_face_detector.h>
using namespace dlib;
using namespace std;
// ------------------------------------------------------------------
template <template <int, template<typename>class, int, typename> class block, int N, template<typename>class BN, typename SUBNET>
using residual = add_prev1<block<N, BN, 1, tag1<SUBNET>>>;
template <template <int, template<typename>class, int, typename> class block, int N, template<typename>class BN, typename SUBNET>
using residual_down = add_prev2<avg_pool<2, 2, 2, 2, skip1<tag2<block<N, BN, 2, tag1<SUBNET>>>>>>;
template <int N, template <typename> class BN, int stride, typename SUBNET>
using block = BN<con<N, 3, 3, 1, 1, relu<BN<con<N, 3, 3, stride, stride, SUBNET>>>>>;
template <int N, typename SUBNET> using ares = relu<residual<block, N, affine, SUBNET>>;
template <int N, typename SUBNET> using ares_down = relu<residual_down<block, N, affine, SUBNET>>;
template <typename SUBNET> using alevel0 = ares_down<256, SUBNET>;
template <typename SUBNET> using alevel1 = ares<256, ares<256, ares_down<256, SUBNET>>>;
template <typename SUBNET> using alevel2 = ares<128, ares<128, ares_down<128, SUBNET>>>;
template <typename SUBNET> using alevel3 = ares<64, ares<64, ares<64, ares_down<64, SUBNET>>>>;
template <typename SUBNET> using alevel4 = ares<32, ares<32, ares<32, SUBNET>>>;
using anet_type = loss_metric<fc_no_bias<128, avg_pool_everything<
alevel0<
alevel1<
alevel2<
alevel3<
alevel4<
max_pool<3, 3, 2, 2, relu<affine<con<32, 7, 7, 2, 2,
input_rgb_image_sized<150>
>>>>>>>>>>>>;
// -----------------------------------------------
int main(int argc, char** argv)
try
{
if (argc != 2)
{
cout << "Run this example by invoking it like this: " << endl;
cout << " ./FaceRecognition QRcode" << endl;
return 1;
}
frontal_face_detector detector = get_frontal_face_detector();
shape_predictor sp;
deserialize("shape_predictor_5_face_landmarks.dat") >> sp;
anet_type net;
deserialize("taguchi_face_recognition_resnet_model_v1.dat") >> net;
string path = "faces/";
path += argv[1];
matrix<rgb_pixel> webcamImg;
std::vector<matrix<rgb_pixel>> faces;
// get QR code image
std::vector<file> QRfiles = get_files_in_directory_tree(path, match_ending(".png"));
for (const auto& QRfile : QRfiles)
{
load_image(webcamImg, QRfile);
for (auto face : detector(webcamImg))
{
auto shape = sp(webcamImg, face);
matrix<rgb_pixel> face_chip;
extract_image_chip(webcamImg, get_face_chip_details(shape, 150, 0.25), face_chip);
faces.push_back(move(face_chip));
break; // Assuming one person
}
break; // Assuming one image
}
if (faces.size() == 0)
{
cout << "No images found in " << path << endl;
return 2;
}
// get webcam image
std::vector<file> imgfiles = get_files_in_directory_tree("faces/temp", match_ending(".jpg"));
for (const auto& imgfile : imgfiles)
{
load_image(webcamImg, imgfile);
for (auto face : detector(webcamImg))
{
auto shape = sp(webcamImg, face);
matrix<rgb_pixel> face_chip;
extract_image_chip(webcamImg, get_face_chip_details(shape, 150, 0.25), face_chip);
faces.push_back(move(face_chip));
}
}
if (faces.size() < 2)
{
cout << "No faces found in image!" << "faces/temp" << endl;
return 3;
}
cout << "face count:" << faces.size() << endl;
std::vector<matrix<float, 0, 1>> face_descriptors = net(faces);
float accuracy = std::numeric_limits<float>::max();
for (size_t j = 1; j < face_descriptors.size(); ++j)
{
cout << "length: " << length(face_descriptors[0] - face_descriptors[j]) << endl;
if (accuracy > length(face_descriptors[0] - face_descriptors[j]))
{
accuracy = length(face_descriptors[0] - face_descriptors[j]);
}
}
ofstream writing_file;
string filename = path + "/accuracy.txt";
writing_file.open(filename, std::ios::out);
string writing_text = to_string(accuracy);
writing_file << writing_text << std::endl;
writing_file.close();
cout << "accuracy: " << accuracy << endl;
cout << "hit enter to terminate" << endl;
//cin.get();
return 0;
}
catch (std::exception& e)
{
cout << e.what() << endl;
return 4;
}
アプリケーションの動作イメージ
「Start」ボタンクリック↓
QRコードをかざす↓
顔を認証中↓
認証結果が 'accuracy is' の右側に表示される↓
顔認証モデルの入手
dlib のモデル 'shape_predictor_5_face_landmarks.dat'は以下のリンクより取得してください。
※ 拡張子に'.bz2'が付いていますので、ダウンロード後に解凍してください
OpenCV のカスケード識別器 'haarcascade_frontalface_default.xml' を以下より取得してください。
Github Reposittories
ソースコードと画像は Github に揃っています
Github Face-recognition-using-QR-code
'taguchi_face_recognition_resnet_model_v1.dat' は下記を参照のこと。
Taguchi models
C++ コンパイル方法
'FaceRecognition.cpp' のコンパイル方法は dlib.net に記載されていますので参考にしてください。
- Visual studio 2022 (V143)
- ISO C++14
- x64, Release
How to compile dlib
C# コンパイル方法
'MainWindow.xaml.cs', 'MainWindow.xaml' のコンパイル方法は以下になります。
- Visual studio 2022
- Target framework .NET8.0
- Nuget:OpenCvSharp4.Windows
- Nuget:OpenCvSharp4.Extensions
- Nuget:OpenCvSharp4.WpfExtensions
- Nuget:ZXing.Net
実行の準備
コンパイル後に出来た 'FaceRecognition.exe' 実行ファイルを C# の実行フォルダーに配置してください。
- 'faces' フォルダーを配置して、その配下に '000100' フォルダーを配置してください
- さらにその配下に '000100.jpg' を配置してください
- 'faces' フォルダーの配下に 'temp' フォルダーを配置して 'YAMAZAKI_Kento' の画像を5枚配置してください
- 'Images' フォルダーを配置して、その配下に camera.png を配置してください
- 'shape_predictor_5_face_landmarks.dat' を配置してください
- 'taguchi_face_recognition_resnet_model_v1.dat' を配置してください
- 'haarcascade_frontalface_default.xml' を配置してください
- WebカメラをPCに接続してください
- Webカメラに映る人物には、充分な明るさを確保してください
- 'Images' フォルダーにある 'TestQrcode.png' を印刷するか、またはスマートフォンに保管しておいてください
QRコードの読み取りの値で事前に 'faces' フォルダーの下にフォルダーを作成しておく必要があります。
(※この例の場合は'000100' フォルダーで作りました。)
そのフォルダーに保管しておく顔画像のファイルも 'QRコードの読み取り結果.png' の形式で画像ファイルを保管しておく必要があります。
実行方法
Visual studio 2022 より実行します。
- 「Start」ボタンをクリックします
- 印刷またはスマートフォンにあるQRコードをWebカメラに向けます
- QRコードを正常に読めたら 'OK! Please face the webcam.' が表示されます
- 'Authenticating, please wait a moment.' が表示されている間、Webカメラに顔を向けてそのまま待ちます
- 'The system has authenticated you.' が表示されたら認証されています
- 認証が終わると画面下にQRコードの読み取り結果と認証精度(accuracy)が表示されます
'FaceRecognition.exe' の単体テストを行う場合は、コマンドプロンプトより以下のコマンドを打ってください。
- C++ 単体テストの例
> FaceRecognition 000100
制限事項
私のモデルには訓練用データが不足しているため、精度が著しく悪い場合があります。
- アフリカ系のルーツを持つ人物
- 18歳未満の人物
- マスクを付けている人物
上記の用途には向かないことがあります。
注意してご利用ください。