画像やビデオでの顔の検出

この記事では、FaceDetector を使用して画像内の顔を検出する方法について説明します。 FaceTracker は、一連のビデオ フレームで顔を経時的に追跡するために最適化されています。

この記事のコードは、Basic Face Detection および Basic Face Tracking サンプルから調整されています。 これらのサンプルをダウンロードして、コンテキストで使用されているコードを確認したり、独自のアプリの開始点としてサンプルを使用したりできます。

1 つの画像内の顔を検出する

FaceDetector クラスを使用すると、静止画像内の 1 つ以上の顔を検出できます。

FaceDetector オブジェクト用のクラス メンバー変数と、画像内で検出される DetectedFace オブジェクトのリスト用のクラス メンバー変数を宣言します。

private FaceDetector? _faceDetector;
private IList<DetectedFace>? _detectedFaces;

顔検出は、さまざまな方法で作成できる SoftwareBitmap オブジェクトで動作します。 この例では、FileOpenPicker を使用して、顔が検出される画像ファイルをユーザーが選択できるようにします。 ソフトウェア ビットマップの操作の詳細については、「 イメージング」を参照してください。

var photoPicker = new FileOpenPicker();
photoPicker.ViewMode = PickerViewMode.Thumbnail;
photoPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
photoPicker.FileTypeFilter.Add(".jpg");
photoPicker.FileTypeFilter.Add(".jpeg");
photoPicker.FileTypeFilter.Add(".png");
photoPicker.FileTypeFilter.Add(".bmp");

// Initialize the picker with the window handle (required for WinUI 3 desktop apps).
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(photoPicker, hwnd);

StorageFile? photoFile = await photoPicker.PickSingleFileAsync();
if (photoFile == null)
{
    return;
}

BitmapDecoder クラスを使用して、イメージ ファイルを SoftwareBitmap にデコードします。 顔検出プロセスは、画像が小さい方が速いので、ソースイメージをより小さなサイズに縮小することができます。 これは、デコード時に BitmapTransform オブジェクトを作成し、ScaledWidth プロパティと ScaledHeight プロパティを設定して、それを GetSoftwareBitmapAsync の呼び出しに渡すことで実行できます。このメソッドは、デコードおよびスケーリングされた SoftwareBitmap を返します。

var fileStream = await photoFile.OpenAsync(FileAccessMode.Read);
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fileStream);

var transform = new BitmapTransform();
const float sourceImageHeightLimit = 1280;

if (decoder.PixelHeight > sourceImageHeightLimit)
{
    float scalingFactor = sourceImageHeightLimit / (float)decoder.PixelHeight;
    transform.ScaledWidth = (uint)Math.Floor(decoder.PixelWidth * scalingFactor);
    transform.ScaledHeight = (uint)Math.Floor(decoder.PixelHeight * scalingFactor);
}

SoftwareBitmap sourceBitmap = await decoder.GetSoftwareBitmapAsync(
    decoder.BitmapPixelFormat,
    BitmapAlphaMode.Premultiplied,
    transform,
    ExifOrientationMode.IgnoreExifOrientation,
    ColorManagementMode.DoNotColorManage);

現在のバージョンでは、 FaceDetector クラスは Gray8 または Nv12 のイメージのみをサポートしています。 SoftwareBitmap クラスは、ビットマップをある形式から別の形式に変換する Convert メソッドを提供します。 次の使用例は、ソース イメージを Gray8 ピクセル形式に変換します (まだその形式でない場合)。 必要に応じて、 GetSupportedBitmapPixelFormats メソッドと IsBitmapPixelFormatSupported メソッドを使用して、サポートされる一連の形式が将来のバージョンで拡張される場合に備えて、実行時にピクセル形式がサポートされているかどうかを判断できます。

// Use FaceDetector.GetSupportedBitmapPixelFormats and IsBitmapPixelFormatSupported
// to dynamically determine supported formats.
const BitmapPixelFormat faceDetectionPixelFormat = BitmapPixelFormat.Gray8;

SoftwareBitmap convertedBitmap;

if (sourceBitmap.BitmapPixelFormat != faceDetectionPixelFormat)
{
    convertedBitmap = SoftwareBitmap.Convert(sourceBitmap, faceDetectionPixelFormat);
}
else
{
    convertedBitmap = sourceBitmap;
}

CreateAsync を呼び出し、DetectFacesAsync を呼び出して FaceDetector オブジェクトをインスタンス化し、適切なサイズにスケーリングされ、サポートされているピクセル形式に変換されたビットマップを渡します。 このメソッドは、DetectedFace オブジェクトの一覧を返します。 ShowDetectedFaces は、画像内の顔の周りに四角形を描画するヘルパー メソッドです。

_faceDetector ??= await FaceDetector.CreateAsync();

_detectedFaces = await _faceDetector.DetectFacesAsync(convertedBitmap);
ShowDetectedFaces(sourceBitmap, _detectedFaces);

顔検出プロセス中に作成されたオブジェクトは必ず破棄してください。

sourceBitmap.Dispose();
fileStream.Dispose();
convertedBitmap.Dispose();

画像を表示し、検出された顔の周りにボックスを描画するには、 Canvas 要素を XAML ページに追加します。

<Canvas x:Name="VisualizationCanvas"
        Grid.Row="2"
        HorizontalAlignment="Stretch"
        VerticalAlignment="Stretch"/>

描画する四角形のスタイルを設定するメンバー変数をいくつか定義します。

private readonly SolidColorBrush _lineBrush = new(Microsoft.UI.Colors.Yellow);
private readonly double _lineThickness = 2.0;
private readonly SolidColorBrush _fillBrush = new(Microsoft.UI.Colors.Transparent);

ShowDetectedFaces ヘルパー メソッドでは、新しい ImageBrush が作成され、ソースイメージを表す SoftwareBitmap から作成された SoftwareBitmapSource にソースが設定されます。 XAML キャンバス コントロールの背景がイメージ ブラシに設定されます。

ヘルパー メソッドに渡される顔の一覧が空でない場合は、 リスト内の各顔をループ処理し、FaceBoxDetectedFace クラスのプロパティを使用して、顔を含む画像内の四角形の位置とサイズを決定します。 Canvas コントロールはソース イメージとは異なるサイズになる可能性が非常に高いため、X 座標と Y 座標の両方と FaceBox の幅と高さの両方に、ソース イメージ サイズと Canvas コントロールの実際のサイズの比率であるスケーリング値を乗算する必要があります。

private async void ShowDetectedFaces(SoftwareBitmap sourceBitmap, IList<DetectedFace> faces)
{
    ImageBrush brush = new();
    SoftwareBitmapSource bitmapSource = new();

    // SoftwareBitmapSource requires Bgra8 with premultiplied alpha.
    SoftwareBitmap displayBitmap = SoftwareBitmap.Convert(
        sourceBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);

    await bitmapSource.SetBitmapAsync(displayBitmap);
    brush.ImageSource = bitmapSource;
    brush.Stretch = Stretch.Fill;
    VisualizationCanvas.Background = brush;

    VisualizationCanvas.Children.Clear();

    if (faces != null)
    {
        double widthScale = sourceBitmap.PixelWidth / VisualizationCanvas.ActualWidth;
        double heightScale = sourceBitmap.PixelHeight / VisualizationCanvas.ActualHeight;

        foreach (DetectedFace face in faces)
        {
            // Create a rectangle element for displaying the face box but since we're
            // using a Canvas we must scale the rectangles according to the image's
            // actual size. The original FaceBox values are saved in the Rectangle's
            // Tag field so we can update the boxes when the Canvas is resized.
            Rectangle box = new()
            {
                Tag = face.FaceBox,
                Width = (uint)(face.FaceBox.Width / widthScale),
                Height = (uint)(face.FaceBox.Height / heightScale),
                Fill = _fillBrush,
                Stroke = _lineBrush,
                StrokeThickness = _lineThickness,
                Margin = new Thickness(
                    (uint)(face.FaceBox.X / widthScale),
                    (uint)(face.FaceBox.Y / heightScale), 0, 0)
            };

            VisualizationCanvas.Children.Add(box);
        }
    }

    displayBitmap.Dispose();
}

一連のフレームで顔を追跡する

ビデオで顔を検出する場合は、実装手順は非常に似ていますが、FaceTracker クラスではなく、FaceDetector クラスを使用する方が効率的です。 FaceTracker は、以前に処理されたフレームに関する情報を使用して、検出プロセスを最適化します。

FaceTracker オブジェクトのクラス変数を宣言します。 この例では、 DispatcherTimer を使用して、定義された間隔で顔追跡を開始します。 SemaphoreSlim は、一度に実行されている顔追跡操作が 1 つだけであることを確認するために使用されます。

private FaceTracker? _faceTracker;
private DispatcherTimer? _frameProcessingTimer;
private readonly SemaphoreSlim _frameProcessingSemaphore = new(1);

顔追跡操作を初期化するには、CreateAsync を呼び出して新しい FaceTracker オブジェクトを作成します。 目的のタイマー間隔を初期化し、タイマーを作成します。 ProcessCurrentVideoFrame ヘルパー メソッドは、指定した間隔が経過するたびに呼び出されます。

_faceTracker = await FaceTracker.CreateAsync();

_frameProcessingTimer = new DispatcherTimer();
_frameProcessingTimer.Interval = TimeSpan.FromMilliseconds(66); // ~15 fps
_frameProcessingTimer.Tick += ProcessCurrentVideoFrame;
_frameProcessingTimer.Start();

ProcessCurrentVideoFrame ヘルパーはタイマーによって非同期的に呼び出されるため、メソッドは最初にセマフォの Wait メソッドを呼び出して、追跡操作が進行中かどうかを確認し、メソッドが顔を検出せずに返すかどうかを確認します。 このメソッドの最後に、セマフォの Release メソッドが呼び出され、 後続の ProcessCurrentVideoFrame の呼び出しが続行されます。

FaceTracker クラスは、VideoFrame オブジェクトで動作します。 VideoFrame を取得する方法は複数あり、実行中の MediaCapture オブジェクトからプレビュー フレームをキャプチャする方法や、IBasicVideoEffect の ProcessFrame メソッドを実装する方法などがあります。 この例では、この操作のプレースホルダーとして、ビデオ フレーム GetLatestFrame を返す未定義のヘルパー メソッドを使用します。 実行中のメディア キャプチャ デバイスのプレビュー ストリームからビデオ フレームを取得する方法については、「 プレビュー フレームを取得する」を参照してください。

FaceDetector と同様に、FaceTracker は限られたピクセル形式のセットをサポートしています。 この例では、指定されたフレームが Nv12 形式でない場合、顔検出を破棄します。

ProcessNextFrameAsync を呼び出して、フレーム内の顔を表す DetectedFace オブジェクトの一覧を取得します。 顔の一覧を取得したら、顔検出のために上記と同じ方法で顔を表示できます。 顔追跡ヘルパー メソッドは UI スレッドで呼び出されないため、 DispatcherQueue.TryEnqueue の呼び出し内で UI の更新を行う必要があることに注意してください。

private async void ProcessCurrentVideoFrame(object? sender, object e)
{
    if (!_frameProcessingSemaphore.Wait(0))
    {
        return;
    }

    VideoFrame currentFrame = await GetLatestFrame();

    // Use FaceDetector.GetSupportedBitmapPixelFormats and IsBitmapPixelFormatSupported
    // to dynamically determine supported formats.
    const BitmapPixelFormat faceDetectionPixelFormat = BitmapPixelFormat.Nv12;

    if (currentFrame.SoftwareBitmap.BitmapPixelFormat != faceDetectionPixelFormat)
    {
        return;
    }

    try
    {
        IList<DetectedFace> detectedFaces =
            await _faceTracker!.ProcessNextFrameAsync(currentFrame);

        var previewFrameSize = new Windows.Foundation.Size(
            currentFrame.SoftwareBitmap.PixelWidth,
            currentFrame.SoftwareBitmap.PixelHeight);

        DispatcherQueue.TryEnqueue(() =>
        {
            SetupVisualization(previewFrameSize, detectedFaces);
        });
    }
    catch (Exception)
    {
        // Face tracking failed — stop the timer so we don't spam exceptions
        // with the placeholder frame source.
        _frameProcessingTimer?.Stop();
        DispatcherQueue.TryEnqueue(() =>
            SetStatus("Face tracking stopped — the placeholder frame source " +
                "does not provide valid video data. In a real app, supply " +
                "frames from MediaCapture."));
    }
    finally
    {
        _frameProcessingSemaphore.Release();
    }

    currentFrame.Dispose();
}