Delphiで顔認証:TEdgeBrowser × face-api.jsによる人物照合システム
Delphiで顔認証システムを構築する際、JavaScriptライブラリ「face-api.js」をWebView2経由で呼び出すことで、
登録済みの正面顔写真と対象画像の特徴点を比較し、同一人物かどうかを判定することが可能になります。
本記事では、TEdgeBrowserを活用した顔認証の実装手順と、特徴点の抽出・ユークリッド距離による照合・結果の受信までを詳細に解説します。
単なる顔検出ではなく、人物照合を行っています。
この例では、閾値として 距離0.45未満 の場合に同一人物としています。
また、本ホームページでは「PAKUTASO」様
https://www.pakutaso.com/
のフリー顔写真を使用させていただいております。
face-api.jsをダウンロードする
https://github.com/justadudewhohacks/face-api.js
の右上にある「code」⇒「Download ZIP」をクリックすると face-api.js-master.zip がダウンロードできます。
画面設計
Delphi IDEを起動し、「ファイル」⇒「Windows VCLアプリケーション -Delphi」をクリックします。
「ファイル」⇒「すべて保存 Ctrl+Shift+S」をクリックして、プロジェクト保存用フォルダを作成して ユニット(Unit1)とプロジェクト(Project1)を保存します
次に、「プロジェクト」⇒「Project1をビルト Shift+F9」をクリックして事前に一度コンパイルしておきます。(フォルダが生成される)
フォームに以下のコンポーネントをドラッグ&ドロップします。
- TButton ×2
- TImage ×2
- TEdgeBrowser ×1
- TListView ×1
- TMemo ×1
- TOpenDialog ×2
「WebView2Loader.dll」ファイル
「WebView2Loader.dll」ファイルを、実行ファイルと同じフォルダ内(プロジェクト保存フォルダ\Win32\Debug)にコピーします
| (A)「Debug」ビルトで 「Windows 32ビット」 |
プロジェクト保存フォルダ\Win32\Debug |
| (B)「Debug」ビルトで 「Windows 64ビット」 |
プロジェクト保存フォルダ\Win64\Debug |
| (C)「Release」ビルトで 「Windows 32ビット」 |
プロジェクト保存フォルダ\Win32\Release |
| (D)「Release」ビルトで 「Windows 64ビット」 |
プロジェクト保存フォルダ\Win64\Release |
HTMLファイル、jsファイルなどの配置
- プロジェクト保存フォルダ\Win32\Debug に「htdocs」フォルダを作成します。
-
プロジェクト保存フォルダ\Win32\Debug\htdocs に、
face-api.js-master.zip ファイルを解凍した フォルダ「face-api.js-master」を丸ごとコピーします。 -
プロジェクト保存フォルダ\Win32\Debug\htdocs に、以下の index.html ファイルをUTF-8で保存します。
SSD-MobileNetV1を使用して顔検出するように設定しています。
SSD-MobileNetV1はCNN(畳み込みニューラルネットワーク)ベースのモデルでDepthwise Separable Convolution を採用した約28層の構成のようです。<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <script src="./face-api.js-master/dist/face-api.min.js"></script> <script> let faceMatcher=[];//顔画像から特徴点を取り出した値を入れる window.addEventListener('DOMContentLoaded',async function(event){ await Promise.all([ //SsdMobilenetv1Modelを使用する faceapi.nets.ssdMobilenetv1.loadFromUri('./face-api.js-master/weights'), //faceLandmark68Netを使用する faceapi.nets.faceLandmark68Net.loadFromUri("./face-api.js-master/weights"), //faceRecognitionNetを使用する faceapi.nets.faceRecognitionNet.loadFromUri("./face-api.js-master/weights") ]); document.getElementById("res").innerHTML="face-api.js loaded."; }); //顔識別させたい正面顔画像を登録する async function registImg(src){ //document.getElementById("res").innerHTML=src; document.getElementById("res").innerHTML=""; let img=await loadImage(src); let singleResult=await faceapi.detectAllFaces(img).withFaceLandmarks().withFaceDescriptors(); let num=faceMatcher.length; if(singleResult.length>0){ const labeledDescriptors = [ new faceapi.LabeledFaceDescriptors( 'label'+num,//ラベルを設定する [singleResult[0].descriptor] ), ]; faceMatcher[num] = new faceapi.FaceMatcher(labeledDescriptors); document.getElementById("res").innerHTML="Registed Image"; } } async function loadImage(src){ return new Promise(function(resolve,reject){ let img=new Image(); img.onload=function(){resolve(img);} img.onerror=function(e){reject(e);} img.src=src; }); } async function descript(src){ document.getElementById("res").innerHTML=""; let img=await loadImage(src); faceapi.detectAllFaces(img, new faceapi.SsdMobilenetv1Options()).withFaceLandmarks().withFaceDescriptors().then( function(result){ let res=null; let dis=1; if(faceMatcher!=null && result.length>0){ for(i=0;i<result.length;i++){ for(let j=0;j<faceMatcher.length;j++){ let bestMatch = faceMatcher[j].findBestMatch(result[i].descriptor); if(bestMatch.distance<dis){ res=result[i]; dis=bestMatch.distance; } if(res){ document.getElementById("res").innerHTML= res.detection.box.x+","+ res.detection.box.y+","+ res.detection.box.width+","+ res.detection.box.height+","+ dis ; }else{ document.getElementById("res").innerHTML="0,0,0,0,1"; } } } } } ); } </script> </head> <body> <h3>face-api.jsサンプル</h3> <div id="res"></div> </body> </html>
以下のようなフォルダ、ファイル構成になります。
| プロジェクト保存フォルダ\Win32\Debug\ | WebView2Loader.dll | ||
| htdocs\ | index.html | ||
| face-api.js-master\ |
dist\・・・ examples\・・・ src\・・・ weights\・・・ ・・・ |
||
ソースコードの記述
以下ソースコードをコピー&ペーストして、IDEから各イベントプロパティを設定します。
| オブジェクト | イベント | プロシージャ |
|---|---|---|
| Form1 | OnCreate | FormCreate |
| Button1 | OnClick | Button1Click |
| Button2 | OnClick | Button2Click |
| EdgeBrowser1 | OnExecuteScript | EdgeBrowser1ExecuteScript |
| ListView1 | OnChange | ListView1Change |
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Winapi.WebView2, Winapi.ActiveX,
Vcl.Edge, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.ComCtrls;
type
TForm1 = class(TForm)
EdgeBrowser1: TEdgeBrowser;
Memo1: TMemo;
Button1: TButton;
Button2: TButton;
OpenDialog1: TOpenDialog;
Image1: TImage;
ListView1: TListView;
OpenDialog2: TOpenDialog;
Image2: TImage;
procedure FormCreate(Sender: TObject);
procedure EdgeBrowser1ExecuteScript(Sender: TCustomEdgeBrowser;
AResult: HRESULT; const AResultObjectAsJson: string);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure ListView1Change(Sender: TObject; Item: TListItem;
Change: TItemChange);
private
{ Private 宣言 }
ResultString:String;
public
{ Public 宣言 }
end;
TDOMContentLoadedHandler = class(TInterfacedObject, ICoreWebView2DOMContentLoadedEventHandler)
function Invoke(const sender: ICoreWebView2;
const args: ICoreWebView2DOMContentLoadedEventArgs): HRESULT; stdcall;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses system.IOUtils, Vcl.Imaging.jpeg, Vcl.Imaging.pngimage, System.NetENcoding;
{ TDOMContentLoadedHandler }
function TDOMContentLoadedHandler.Invoke(const sender: ICoreWebView2;
const args: ICoreWebView2DOMContentLoadedEventArgs): HRESULT;
begin
Form1.Button1.Enabled:=True;
Form1.button2.Enabled:=False;
Result := S_OK;
end;
{ TForm1 }
procedure TForm1.Button1Click(Sender: TObject);
var jpg:TJpegImage;
bmp:TBitmap;
png:TPngImage;
strm:TMemoryStream;
b:TBytes;
st:string;
FileName:String;
ext:string;
begin
if not OpenDialog1.Execute(self.Handle) then exit;
Button1.Enabled:=False;
FileName:=OpenDialog1.FileName;
ext:=LowerCase(ExtractFileExt(FileName));
Image1.Picture.LoadFromFile(FileName);
jpg:=TJPEGImage.Create;
bmp:=TBitmap.create;
png:=TPngImage.create;
strm:=TMemoryStream.Create;
try
if (ext='.jpg') or (ext='.jpeg') then
begin
jpg.LoadFromFile(FileName);
bmp.Assign(jpg);
png.Assign(bmp);
end
else
begin
png.LoadFromFile(FileName);
end;
png.SaveToStream(strm);
strm.Position:=0;
setlength(b, strm.Size);
strm.ReadData(@b[0], strm.Size);
//PNG画像データをData URIスキーム(Base64)形式に変換してJavascript関数へ送る
st:='data:image/png;base64,'+
System.NetEncoding.TNetEncoding.Base64.EncodeBytesToString(b);
st:=StringReplace(st,#13#10,'',[rfReplaceAll]);
Memo1.Lines.Add('認証画像登録中・・・');
ResultString:='';
EdgeBrowser1.ExecuteScript('registImg("'+st+'");');
while (ResultString<>'Registed Image') do
begin
EdgeBrowser1.ExecuteScript('document.getElementById("res").innerHTML');
Sleep(100);
Application.ProcessMessages;
end;
Memo1.Lines.Add('認証画像登録完了');
Button2.Enabled:=True;
finally
jpg.free;
png.Free;
bmp.free;
strm.free;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var jpg:TJpegImage;
bmp:TBitmap;
png:TPngImage;
strm:TMemoryStream;
b:TBytes;
st:string;
i:Integer;
ext:string;
begin
if not OpenDialog2.execute then exit;
ListView1.Clear;
for i := 0 to OpenDialog2.Files.Count-1 do
begin
ListView1.AddItem(OpenDialog2.Files[i],nil);
end;
jpg:=TJPEGImage.Create;
bmp:=TBitmap.create;
png:=TPngImage.create;
strm:=TMemoryStream.Create;
try
for i := 0 to ListView1.Items.Count-1 do
begin
ext:=LowerCase(ExtractFileExt(Listview1.items[i].Caption));
if (ext='.jpg') or (ext='.jpeg') then
begin
jpg.LoadFromFile(ListView1.items[i].Caption);
bmp.Assign(jpg);
png.Assign(bmp);
end
else
begin
png.LoadFromFile(ListView1.Items[i].Caption);
end;
strm.size:=0;
png.SaveToStream(strm);
strm.Position:=0;
setlength(b, strm.Size);
strm.ReadData(@b[0], strm.Size);
//PNG画像データをData URIスキーム(Base64)形式に変換してJavascript関数へ送る
st:='data:image/png;base64,'+
System.NetEncoding.TNetEncoding.Base64.EncodeBytesToString(b);
st:=StringReplace(st,#13#10,'',[rfReplaceAll]);
Memo1.Lines.Add(ListView1.Items[i].Caption);
Memo1.Lines.Add('の顔を照合中');
ResultString:='';
EdgeBrowser1.ExecuteScript('descript("'+st+'");');
while (ResultString='') do
begin
EdgeBrowser1.ExecuteScript('document.getElementById("res").innerHTML');
Sleep(100);
Application.ProcessMessages;
end;
ListView1.items[i].SubItems.CommaText:=ResultString;
EdgeBrowser1.ExecuteScript('document.getElementById("res").innerHTML="";');
while (ResultString<>'') do
begin
EdgeBrowser1.ExecuteScript('document.getElementById("res").innerHTML');
Sleep(100);
Application.ProcessMessages;
end;
end;
finally
jpg.free;
png.Free;
bmp.free;
strm.free;
end;
ListView1.ItemIndex:=0;
end;
procedure TForm1.EdgeBrowser1ExecuteScript(Sender: TCustomEdgeBrowser;
AResult: HRESULT; const AResultObjectAsJson: string);
var st:String;
begin
//戻り値が無い関数(例えばScan)を呼出した場合AResultObjectAsJsonの値は'{}'
if AResultObjectAsJson='{}' then
begin
ResultString:='';
end
else if AResultObjectAsJson<>'null' then
begin
st:=AResultObjectAsJson;
if st.Substring(0,1)='"' then
st:=st.Substring(1,Length(st)-1);
if st.Substring(Length(st)-1,1)='"' then
st:=st.Substring(0,Length(st)-1);
st:=StringReplace(st,'\"','"',[rfReplaceAll]);
ResultString:=st;
end
else
begin
ResultString:='';
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
var cachepath:string;
Handler: ICoreWebView2DOMContentLoadedEventHandler;
token:EventRegistrationToken;
col:TListColumn;
begin
Button1.Enabled:=False;
Button2.Enabled:=False;
Button1.Caption:='顔認証したい顔写真登録';
Button1.Width:=Form1.canvas.TextWidth(Button1.Caption)+8;
Button2.Caption:='調べたい複数の写真を選択';
Button2.Width:=Form1.canvas.TextWidth(Button2.Caption)+8;
OpenDialog1.Filter:='jpeg又はpng|*.jpg;*.jpeg;*.png';
OpenDialog2.Filter:=OpenDialog1.Filter;
OpenDialog2.Options:=OpenDialog2.Options+[ofAllowMultiSelect];
ListView1.ViewStyle:=vsReport;
ListView1.RowSelect:=True;
col:=ListView1.columns.Add;
col.Caption:='ファイル名';
col.Width:=128;
col:=ListView1.columns.Add;
col.Caption:='x';
col.Width:=32;
col:=ListView1.columns.Add;
col.Caption:='y';
col.Width:=32;
col:=ListView1.columns.Add;
col.Caption:='w';
col.Width:=32;
col:=ListView1.columns.Add;
col.Caption:='h';
col.Width:=32;
col:=ListView1.columns.Add;
col.Caption:='distance';
col.Width:=128;
Image1.Proportional:=True;
Image1.Stretch:=True;
Image2.Proportional:=True;
Image2.Stretch:=True;
//キャッシュのフォルダ位置
cachepath:=ExtractFilePath(Application.ExeName)+'cache';
//キャッシュを削除する
if DirectoryExists(cachepath) then TDirectory.Delete(cachepath,true);
//キャッシュのフォルダを指定する
//ここに作成される「EBWebView」フォルダを削除すればキャッシュを消せる
EdgeBrowser1.UserDataFolder:=cachepath;
//WebViewの生成待ち
if not EdgeBrowser1.WebViewCreated then
begin
EdgeBrowser1.CreateWebView;
while not EdgeBrowser1.WebViewCreated do
begin
sleep(100);
application.ProcessMessages;
end;
end;
// https://hoge/ を .\htdocsフォルダに割り当て
ICoreWebView2_3(EdgeBrowser1.DefaultInterface).SetVirtualHostNameToFolderMapping(
'hoge',
PWideChar(ExtractFilePath(Application.ExeName)+'htdocs'),
COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_ALLOW
);
//DOMContentLoadedイベントの割り当て
Handler := TDOMContentLoadedHandler.Create;
ICoreWebView2_2(EdgeBrowser1.DefaultInterface).add_DOMContentLoaded(
Handler, token
);
//ローカルファイル \hoge\index.htmlを読む
EdgeBrowser1.Navigate('https://hoge/index.html');
end;
procedure TForm1.ListView1Change(Sender: TObject; Item: TListItem;
Change: TItemChange);
var ext:string;
jpg:TJpegImage;
bmp:TBitmap;
png:TPngImage;
x,y,w,h,dist:Single;
begin
if ListView1.ItemIndex>=0 then
begin
ext:=LowerCase(ExtractFileExt(ListView1.Items[ListView1.ItemIndex].Caption));
jpg:=TJpegImage.Create;
bmp:=TBitmap.Create;
png:=TPngImage.Create;
try
if (ext='.jpg') or (ext='.jpeg') then
begin
jpg.LoadFromFile(ListView1.Items[ListView1.ItemIndex].Caption);
bmp.Assign(jpg);
end
else
begin
png.LoadFromFile(ListView1.Items[ListView1.ItemIndex].Caption);
bmp.Assign(png);
end;
x:=StrToFloatDef(ListView1.Items[ListView1.ItemIndex].SubItems[0],0);
y:=StrToFloatDef(ListView1.Items[ListView1.ItemIndex].SubItems[1],0);
w:=StrToFloatDef(ListView1.Items[ListView1.ItemIndex].SubItems[2],0);
h:=StrToFloatDef(ListView1.Items[ListView1.ItemIndex].SubItems[3],0);
dist:=StrToFloatDef(ListView1.Items[ListView1.ItemIndex].SubItems[4],0);
Image2.Picture.Bitmap.Assign(bmp);
//★★★距離が4.5未満は同一人物とみなす
if dist<0.45 then
begin
Image2.Picture.Bitmap.Canvas.pen.Color:=clRed;
Image2.Picture.Bitmap.Canvas.pen.Width:=4;
Image2.Picture.Bitmap.Canvas.brush.Style:=bsClear;
Image2.Picture.Bitmap.Canvas.Rectangle(
trunc(x), trunc(y), trunc(x+w), trunc(y+h)
);
end;
finally
jpg.Free;
bmp.Free;
png.Free;
end;
end;
end;
end.
実行する
実行し、しばらく待ちます。(face-api.jsのロード待ち)
「顔認証したい顔写真登録」ボタンをクリックします。
顔認証したい正面顔写真の.jpg 又は .jpg ファイルを選択します。
20秒程度待つと、データのロードと正面顔写真の登録が完了します。
「調べたい複数の写真を選択」をクリックします。
同じ人物が写っている写真、写っていない写真などをCtrlやShiftキーを押しながらクリックして複数写真を選びます。
しばらく待つと、各写真に認証したい顔の人物が写っている可能性を distance(0~1)として計算結果が表示されます。
このソースコードでは、distance(距離)が 0.45未満 の場合に同一人物としています。
