Detectar rostos em imagens ou vídeos

Este artigo mostra como usar o FaceDetector para detectar rostos em uma imagem. O FaceTracker é otimizado para rastrear rostos ao longo do tempo em uma sequência de quadros de vídeo.

O código neste artigo foi adaptado dos exemplos Basic Face Detection e Basic Face Tracking. Você pode baixar esses exemplos para ver o código usado no contexto ou para usar o exemplo como ponto de partida para seu próprio aplicativo.

Detectar rostos em uma única imagem

A classe FaceDetector permite detectar um ou mais rostos em uma imagem parada.

Declare uma variável de membro de classe para o objeto FaceDetector e para a lista de objetos DetectedFace que serão detectados na imagem.

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

A detecção facial opera em um objeto SoftwareBitmap que pode ser criado de várias maneiras. Neste exemplo, um FileOpenPicker é usado para permitir que o usuário escolha um arquivo de imagem no qual os rostos serão detectados. Para obter mais informações sobre como trabalhar com bitmaps de software, consulte Imagens.

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;
}

Use a classe BitmapDecoder para decodificar o arquivo de imagem em um SoftwareBitmap. O processo de detecção facial é mais rápido com uma imagem menor e, portanto, talvez você queira reduzir a imagem de origem para um tamanho menor. Isso pode ser feito durante a decodificação criando um objeto BitmapTransform, definindo as propriedades ScaledWidth e ScaledHeight e passando-o para a chamada de GetSoftwareBitmapAsync, que retorna o SoftwareBitmap decodificado e dimensionado.

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);

Na versão atual, a classe FaceDetector dá suporte apenas a imagens em Gray8 ou Nv12. A classe SoftwareBitmap fornece o método Convert , que converte um bitmap de um formato para outro. Este exemplo converterá a imagem de origem no formato de pixel Gray8 se ela ainda não estiver nesse formato. Se desejar, você pode usar os métodos GetSupportedBitmapPixelFormats e IsBitmapPixelFormatSupported para determinar em runtime se há suporte para um formato de pixel, caso o conjunto de formatos com suporte seja expandido em versões futuras.

// 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;
}

Instancie o objeto FaceDetector chamando CreateAsync e, em seguida, chamando DetectFacesAsync, passando o bitmap que foi dimensionado para um tamanho razoável e convertido em um formato de pixel com suporte. Esse método retorna uma lista de objetos DetectedFace. ShowDetectedFaces é um método auxiliar, mostrado abaixo, que desenha quadrados ao redor dos rostos na imagem.

_faceDetector ??= await FaceDetector.CreateAsync();

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

Certifique-se de descartar os objetos que foram criados durante o processo de detecção facial.

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

Para exibir a imagem e desenhar caixas ao redor dos rostos detectados, adicione um elemento Canvas à página XAML.

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

Defina algumas variáveis de membro para estilizar os quadrados que serão desenhados.

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

No método auxiliar ShowDetectedFaces , um novo ImageBrush é criado e a origem é definida como um SoftwareBitmapSource criado a partir do SoftwareBitmap que representa a imagem de origem. O plano de fundo do controle XAML Canvas é definido como o pincel de imagem.

Se a lista de rostos passados para o método auxiliar não estiver vazia, faça loop em cada face da lista e use a propriedade FaceBox da classe DetectedFace para determinar a posição e o tamanho do retângulo dentro da imagem que contém o rosto. Como o controle Canvas é muito provável que seja um tamanho diferente da imagem de origem, você deve multiplicar as coordenadas X e Y e a largura e altura do FaceBox por um valor de dimensionamento que é a proporção do tamanho da imagem de origem para o tamanho real do controle 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();
}

Acompanhar rostos em uma sequência de quadros

Se você quiser detectar rostos em vídeo, é mais eficiente usar a classe FaceTracker em vez da classe FaceDetector, embora as etapas de implementação sejam muito semelhantes. O FaceTracker usa informações sobre quadros processados anteriormente para otimizar o processo de detecção.

Declare uma variável de classe para o objeto FaceTracker . Este exemplo usa um DispatcherTimer para iniciar o acompanhamento facial em um intervalo definido. Um SemaphoreSlim é usado para garantir que apenas uma operação de rastreamento facial esteja em execução por vez.

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

Para inicializar a operação de acompanhamento facial, crie um novo objeto FaceTracker chamando CreateAsync. Inicialize o intervalo de temporizador desejado e crie o temporizador. O método auxiliar ProcessCurrentVideoFrame será chamado sempre que o intervalo especificado decorrer.

_faceTracker = await FaceTracker.CreateAsync();

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

O auxiliar ProcessCurrentVideoFrame é chamado de forma assíncrona pelo temporizador, portanto, o método primeiro chama o método Wait do semáforo para ver se uma operação de rastreamento está em andamento e se é o método retorna sem tentar detectar rostos. Ao final deste método, o método Release do semáforo é chamado, o que permite que a chamada subsequente para ProcessCurrentVideoFrame continue.

A classe FaceTracker opera em objetos VideoFrame. Há várias maneiras de obter um VideoFrame, incluindo capturar um quadro de visualização de um objeto MediaCapture em execução ou implementar o método ProcessFrame do IBasicVideoEffect. Este exemplo usa um método auxiliar indefinido que retorna um quadro de vídeo, GetLatestFrame, como um espaço reservado para esta operação. Para obter informações sobre como obter quadros de vídeo do fluxo de visualização de um dispositivo de captura de mídia em execução, consulte Obter um quadro de visualização.

Assim como acontece com o FaceDetector, o FaceTracker dá suporte a um conjunto limitado de formatos de pixel. Este exemplo abandonará a detecção facial se o quadro fornecido não estiver no formato Nv12.

Chame ProcessNextFrameAsync para recuperar uma lista de objetos DetectedFace que representam os rostos no quadro. Depois de ter a lista de rostos, você poderá exibi-los da mesma maneira descrita acima para detecção facial. Observe que, como o método auxiliar de rastreamento facial não é chamado na thread da interface do usuário, você deve fazer qualquer atualização na interface do usuário dentro de uma chamada a DispatcherQueue.TryEnqueue.

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();
}