Usar OpenCV con MediaFrameReader

En este artículo se muestra cómo usar la biblioteca openCV (open source Computer Vision) con un objeto MediaFrameReader para procesar fotogramas de cámara en tiempo real. Esta técnica es útil para tareas de análisis de imágenes, como la detección de bordes, el seguimiento de objetos y la sustracción de fondo en una aplicación de escritorio WinUI 3.

Prerequisites

Antes de comenzar, asegúrese de que tiene:

  • Un proyecto de escritorio de WinUI 3 (empaquetado o desempaquetado).
  • Una cámara web compatible conectada al dispositivo.
  • Los paquetes NuGet OpenCvSharp4 y OpenCvSharp4.runtime.win están instalados en su proyecto.
  • Acceso a la cámara declarado para la aplicación, como se describe en Declaración del acceso a la cámara.

Para instalar OpenCV para .NET:

dotnet add package OpenCvSharp4
dotnet add package OpenCvSharp4.runtime.win

Declaración del acceso a la cámara

Antes de que la aplicación pueda usar MediaFrameReader para leer fotogramas desde una cámara web, debes declarar que la aplicación usa la cámara. La forma de hacerlo depende de si la aplicación está empaquetada.

  • Aplicaciones empaquetadas: agregue la funcionalidad del webcam dispositivo a Package.appxmanifest:

    <Capabilities>
        <DeviceCapability Name="webcam" />
    </Capabilities>
    
  • Aplicaciones sin empaquetar: las aplicaciones sin empaquetar no tienen un manifiesto de paquete, por lo que no hay ninguna capacidad para declarar. El acceso a la cámara para aplicaciones sin empaquetar se controla mediante la opción Permitir que las aplicaciones de escritorio accedan a la configuración de la cámara en Configuración>Privacidad y seguridad>Cámara en el dispositivo del usuario.

En ambos casos, el usuario todavía puede denegar el acceso a la cámara en el nivel de sistema operativo. Compruebe el acceso antes de inicializar la cámara y controle el caso denegado correctamente, como se describe en Controlar la configuración de privacidad de la cámara Windows.

Configuración de MediaFrameReader

En primer lugar, cree una MediaCapture instancia y busque un origen de fotogramas de vídeo de color. A continuación, cree un MediaFrameReader para recibir fotogramas:

using System.Linq;
using Windows.Media.Capture;
using Windows.Media.Capture.Frames;
using Windows.Graphics.Imaging;

private MediaCapture _mediaCapture;
private MediaFrameReader _frameReader;

private async Task InitializeFrameReaderAsync()
{
    // Find a color video source group
    var sourceGroups =
        await MediaFrameSourceGroup.FindAllAsync();

    var selectedGroup = sourceGroups.FirstOrDefault(group =>
        group.SourceInfos.Any(info =>
            info.MediaStreamType == MediaStreamType.VideoPreview &&
            info.SourceKind == MediaFrameSourceKind.Color));

    if (selectedGroup == null)
    {
        // No suitable camera found
        return;
    }

    _mediaCapture = new MediaCapture();
    var settings = new MediaCaptureInitializationSettings
    {
        SourceGroup = selectedGroup,
        MemoryPreference = MediaCaptureMemoryPreference.Cpu,
        StreamingCaptureMode = StreamingCaptureMode.Video,
    };

    await _mediaCapture.InitializeAsync(settings);

    // Find the color video source
    var colorSource = _mediaCapture.FrameSources
        .Values.FirstOrDefault(source =>
            source.Info.SourceKind == MediaFrameSourceKind.Color);

    if (colorSource == null)
    {
        return;
    }

    _frameReader =
        await _mediaCapture.CreateFrameReaderAsync(colorSource);
    _frameReader.FrameArrived += FrameReader_FrameArrived;

    await _frameReader.StartAsync();
}

Importante

Establezca MemoryPreference en Cpu para que los fotogramas lleguen como objetos SoftwareBitmap a los que puede acceder directamente desde código administrado.

Procesar fotogramas con OpenCV

En el controlador de eventos FrameArrived, obtenga el SoftwareBitmap del fotograma y conviértalo en un Mat de OpenCV para su procesamiento. Necesita la interfaz COM IMemoryBufferByteAccess para acceder a los datos de píxeles sin procesar. Agregue esta definición al proyecto:

using System.Runtime.InteropServices;

[ComImport]
[System.Runtime.InteropServices.Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

A continuación, procese los fotogramas:

using OpenCvSharp;

private void FrameReader_FrameArrived(
    MediaFrameReader sender,
    MediaFrameArrivedEventArgs args)
{
    using var frameRef =
        sender.TryAcquireLatestFrame();
    if (frameRef == null) return;

    var bitmap = frameRef.VideoMediaFrame?.SoftwareBitmap;
    if (bitmap == null) return;

    // Convert to Bgra8 format if needed
    SoftwareBitmap convertedBitmap = null;
    if (bitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8)
    {
        convertedBitmap = SoftwareBitmap.Convert(
            bitmap, BitmapPixelFormat.Bgra8);
        bitmap = convertedBitmap;
    }

    // Access the pixel buffer
    using var buffer = bitmap.LockBuffer(
        BitmapBufferAccessMode.Read);
    using var reference =
        buffer.CreateReference();

    // Get the buffer as a byte array via
    // IMemoryBufferByteAccess
    unsafe
    {
        ((IMemoryBufferByteAccess)reference).GetBuffer(
            out byte* dataInBytes,
            out uint capacity);

        // Use stride from the buffer plane description
        // to avoid corrupted images from padding
        var desc = buffer.GetPlaneDescription(0);
        using var mat = Mat.FromPixelData(
            desc.Height, desc.Width,
            MatType.CV_8UC4,
            (IntPtr)dataInBytes,
            desc.Stride);

        // Process the frame with OpenCV
        ProcessFrame(mat);
    }

    // Dispose the converted bitmap if we created one
    convertedBitmap?.Dispose();
}

Note

El bloque de código no seguro usa IMemoryBufferByteAccess para obtener un puntero directo a los datos del mapa de bits. Debe habilitar el código no seguro en la configuración del proyecto añadiendo <AllowUnsafeBlocks>true</AllowUnsafeBlocks> al archivo .csproj.

Aplicación de operaciones de OpenCV

Puede aplicar cualquier operación de procesamiento de imágenes de OpenCV a Mat. Este es un ejemplo que aplica la detección perimetral de Canny:

private SoftwareBitmap _processedBitmap;

private void ProcessFrame(Mat inputMat)
{
    // Convert to grayscale for edge detection
    using var grayMat = new Mat();
    Cv2.CvtColor(inputMat, grayMat, ColorConversionCodes.BGRA2GRAY);

    // Apply Canny edge detection
    using var edges = new Mat();
    Cv2.Canny(grayMat, edges, 50, 200);

    // Convert back to BGRA for display
    using var outputMat = new Mat();
    Cv2.CvtColor(edges, outputMat, ColorConversionCodes.GRAY2BGRA);

    // Create a SoftwareBitmap from the processed Mat
    var processedBitmap = new SoftwareBitmap(
        BitmapPixelFormat.Bgra8,
        outputMat.Width,
        outputMat.Height,
        BitmapAlphaMode.Premultiplied);

    using var destBuffer = processedBitmap.LockBuffer(
        BitmapBufferAccessMode.Write);
    using var destRef = destBuffer.CreateReference();

    unsafe
    {
        ((IMemoryBufferByteAccess)destRef).GetBuffer(
            out byte* destBytes, out uint destCapacity);

        // Copy the processed data to the SoftwareBitmap row by row.
        // outputMat.Step() and desc.Stride can differ (for example,
        // because of OpenCV's own row alignment), so a single bulk
        // copy across the whole buffer can corrupt the image. Copying
        // one row at a time, using each buffer's own stride, avoids
        // that mismatch.
        var desc = destBuffer.GetPlaneDescription(0);
        int rowBytes = outputMat.Width * outputMat.ElemSize();
        byte* sourceRow = (byte*)outputMat.Data.ToPointer();
        byte* destRow = destBytes;

        for (int row = 0; row < outputMat.Height; row++)
        {
            System.Buffer.MemoryCopy(
                sourceRow,
                destRow,
                destCapacity - (uint)(row * desc.Stride),
                (uint)rowBytes);

            sourceRow += outputMat.Step();
            destRow += desc.Stride;
        }
    }

    // Update the UI with the processed frame
    DispatcherQueue.TryEnqueue(async () =>
    {
        var source =
            new Microsoft.UI.Xaml.Media.Imaging
                .SoftwareBitmapSource();
        await source.SetBitmapAsync(processedBitmap);
        ProcessedImage.Source = source;

        _processedBitmap?.Dispose();
        _processedBitmap = processedBitmap;
    });
}

Agregue un Image control a su XAML para mostrar los fotogramas procesados:

<Image x:Name="ProcessedImage" Stretch="Uniform" />

Limpieza de recursos

Detenga el lector de fotogramas y libere los recursos cuando haya terminado:

private async Task CleanupAsync()
{
    if (_frameReader != null)
    {
        _frameReader.FrameArrived -= FrameReader_FrameArrived;
        await _frameReader.StopAsync();
        _frameReader.Dispose();
    }

    _mediaCapture?.Dispose();
    _processedBitmap?.Dispose();
}