Verwenden von OpenCV mit „MediaFrameReader”

In diesem Artikel wird gezeigt, wie Sie die OpenCV-Bibliothek (Open Source Maschinelles Sehen) mit einem MediaFrameReader verwenden, um Kameraframes in Echtzeit zu verarbeiten. Diese Technik ist nützlich für Bildanalyseaufgaben wie Edgeerkennung, Objektverfolgung und Hintergrunduntertraktion in einer WinUI 3-Desktop-App.

Prerequisites

Bevor Sie beginnen, stellen Sie sicher, dass Sie folgendes haben:

  • Ein WinUI 3-Desktopprojekt (verpackt oder entpackt).
  • Eine kompatible Webcam, die mit Ihrem Gerät verbunden ist.
  • Die in Ihrem Projekt installierten OpenCvSharp4 - und OpenCvSharp4.runtime.win NuGet-Pakete.
  • Für Ihre App deklarierter Kamerazugriff, wie unter "Kamerazugriff deklarieren" beschrieben.

So installieren Sie OpenCV für .NET:

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

Deklarieren des Kamerazugriffs

Bevor Ihre App zum Lesen von Frames aus einer Webcam verwenden MediaFrameReader kann, müssen Sie deklarieren, dass Ihre App die Kamera verwendet. Wie Sie dies tun, hängt davon ab, ob Ihre App verpackt ist.

  • Verpackte Apps: Hinzufügen der webcam Gerätefunktion zu Package.appxmanifest:

    <Capabilities>
        <DeviceCapability Name="webcam" />
    </Capabilities>
    
  • Entpackte Apps: Entpackte Apps verfügen nicht über ein Paketmanifest, daher gibt es keine Möglichkeit zum Deklarieren. Der Kamerazugriff für nicht paketierte Apps wird stattdessen durch die Einstellung Desktop-Apps den Zugriff auf Ihre Kamera erlauben unter Einstellungen>Datenschutz & Sicherheit>Kamera auf dem Gerät des Nutzers gesteuert.

In beiden Fällen kann der Benutzer weiterhin den Kamerazugriff auf Betriebssystemebene verweigern. Überprüfen Sie den Zugriff, bevor Sie die Kamera initialisieren und den verweigerten Fall ordnungsgemäß behandeln, wie in der Datenschutzeinstellung für die Windows Kamera beschrieben.

Einrichten des MediaFrameReader

Erstellen Sie zunächst eine MediaCapture Instanz, und suchen Sie eine Farbvideoframequelle. Erstellen Sie dann ein MediaFrameReader, um Frames zu empfangen:

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

Important

Legen Sie MemoryPreference auf Cpu fest, damit Frames als SoftwareBitmap-Objekte ankommen, auf die Sie direkt aus verwaltetem Code zugreifen können.

Verarbeiten von Frames mit OpenCV

Rufen Sie im Ereignishandler FrameArrived das SoftwareBitmap aus dem Frame ab und konvertieren Sie es zur Verarbeitung in ein OpenCV-Mat. Sie benötigen die IMemoryBufferByteAccess-COM-Schnittstelle , um auf die Rohpixeldaten zuzugreifen. Fügen Sie ihrem Projekt diese Definition hinzu:

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

Verarbeiten Sie dann die Frames:

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

Der unsichere Codeblock verwendet IMemoryBufferByteAccess , um einen direkten Zeiger auf die Bitmapdaten abzurufen. Sie müssen unsicheren Code in Ihren Projekteinstellungen aktivieren, indem Sie <AllowUnsafeBlocks>true</AllowUnsafeBlocks> zu Ihrer .csproj-Datei hinzufügen.

Anwenden von OpenCV-Vorgängen

Sie können jede OpenCV-Bildverarbeitungsoperation auf Mat anwenden. Hier ist ein Beispiel, das die Canny-Edgeerkennung anwendet:

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

Fügen Sie Ihrem XAML-Code ein Image Steuerelement hinzu, um die verarbeiteten Frames anzuzeigen:

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

Bereinigen von Ressourcen

Beenden Sie den Framereader, und geben Sie Ressourcen frei, wenn Sie fertig sind:

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

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