Använda OpenCV med MediaFrameReader

Den här artikeln visar hur du använder OpenCV-biblioteket (open source Visuellt innehåll) med en MediaFrameReader för att bearbeta kameraramar i realtid. Den här tekniken är användbar för bildanalysuppgifter som gränsidentifiering, objektspårning och bakgrundsundertraktion i en WinUI 3-skrivbordsapp.

Förutsättningar

Innan du kan börja bör du kontrollera att du har:

  • Ett WinUI 3-skrivbordsprojekt (paketerat eller uppackat).
  • En kompatibel webbkamera som är ansluten till enheten.
  • NuGet-paketen OpenCvSharp4 och OpenCvSharp4.runtime.win installerade i projektet.
  • Kameraåtkomst deklarerad för din app enligt beskrivningen i Deklarera kameraåtkomst.

Så här installerar du OpenCV för .NET:

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

Deklarera kameraåtkomst

Innan appen kan använda MediaFrameReader för att läsa ramar från en webbkamera måste du deklarera att din app använder kameran. Hur du gör detta beror på om din app är paketerad.

  • Paketerade appar: Lägg till enhetsfunktionen i webcamPackage.appxmanifest:

    <Capabilities>
        <DeviceCapability Name="webcam" />
    </Capabilities>
    
  • Uppackade appar: Uppackade appar har inget paketmanifest, så det finns ingen möjlighet att deklarera. Kameraåtkomst för uppackade appar styrs i stället av inställningen Låt skrivbordsappar komma åt kameran under Inställningar>Sekretess och säkerhetskamera> på användarens enhet.

I båda fallen kan användaren fortfarande neka kameraåtkomst på OS-nivå. Kontrollera åtkomsten innan du initierar kameran och hantera det nekade ärendet på ett smidigt sätt, enligt beskrivningen i Hantera sekretessinställningen för Windows kamera.

Konfigurera MediaFrameReader

Börja med att skapa en MediaCapture instans och hitta en färgvideoramkälla. Skapa sedan en MediaFrameReader för att ta emot ramar:

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

Ange MemoryPreference till Cpu så att bildrutor tas emot som SoftwareBitmap-objekt som du kan komma åt direkt från hanterad kod.

Bearbeta ramar med OpenCV

FrameArrived I händelsehanteraren hämtar du SoftwareBitmap från ramen och konverterar den till en OpenCV Mat för bearbetning. Du behöver COM-gränssnittet IMemoryBufferByteAccess för att få åtkomst till rådata för pixeln. Lägg till den här definitionen i projektet:

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

Bearbeta sedan ramarna:

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

Det osäkra kodblocket använder IMemoryBufferByteAccess för att hämta en direkt pekare till bitmapdata. Du måste aktivera osäker kod i projektinställningarna genom att lägga <AllowUnsafeBlocks>true</AllowUnsafeBlocks> till i .csproj filen.

Tillämpa OpenCV-åtgärder

Du kan tillämpa alla OpenCV-avbildningsbearbetningsåtgärder på Mat. Här är ett exempel som använder Canny-kantdetektering:

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

Lägg till en Image kontroll i XAML för att visa de bearbetade ramarna:

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

Rensa resurser

Stoppa ramläsaren och frigör resurserna när du är klar:

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

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