Použití OpenCV s MediaFrameReader

Tento článek ukazuje, jak pomocí knihovny OpenCV (Open Source Počítačové zpracování obrazu) s MediaFrameReader zpracovávat snímky fotoaparátu v reálném čase. Tato technika je užitečná pro úlohy analýzy obrázků, jako je detekce okrajů, sledování objektů a odečítání na pozadí v desktopové aplikaci WinUI 3.

Předpoklady

Než začnete, ujistěte se, že máte:

  • Desktopový projekt WinUI 3 (zabalený nebo rozbalený)
  • Kompatibilní webová kamera připojená k vašemu zařízení.
  • Balíčky NuGet OpenCvSharp4 a OpenCvSharp4.runtime.win nainstalované v projektu.
  • Přístup k fotoaparátu deklarovaný pro vaši aplikaci, jak je popsáno v části Deklarování přístupu k fotoaparátu.

Instalace OpenCV pro .NET:

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

Deklarace přístupu k fotoaparátu

Než bude vaše aplikace moct číst MediaFrameReader snímky z webové kamery, musíte deklarovat, že vaše aplikace používá fotoaparát. Způsob, jakým to uděláte, závisí na tom, jestli je vaše aplikace zabalená.

  • Zabalené aplikace: Přidejte webcam funkci zařízení do Package.appxmanifest:

    <Capabilities>
        <DeviceCapability Name="webcam" />
    </Capabilities>
    
  • Rozbalené aplikace: Nevybalené aplikace nemají manifest balíčku, takže není k dispozici žádná možnost deklarovat. Přístup k fotoaparátu pro nezabalené aplikace se místo toho řídí nastavením Povolit desktopovým aplikacím přístup k fotoaparátu v části Nastavení>soukromí a zabezpečení>Fotoaparátu na zařízení uživatele.

V obou případech může uživatel dál odepřít přístup k fotoaparátu na úrovni operačního systému. Než inicializujete kameru, zkontrolujte přístup a řádně zpracujte zamítnutý případ, jak je popsáno v nastavení ochrany osobních údajů Windows fotoaparátu.

Nastavte MediaFrameReader

Nejprve vytvořte MediaCapture instanci a vyhledejte zdroj barevného rámečku videa. Pak vytvořte rámec MediaFrameReader pro příjem rámců:

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

Nastavte MemoryPreference na Cpu, aby snímky přicházely jako objekty SoftwareBitmap, ke kterým můžete přistupovat přímo ze spravovaného kódu.

Zpracování snímků pomocí OpenCV

V obslužné rutině události FrameArrived získejte SoftwareBitmap z rámce a převeďte jej na OpenCV Mat pro zpracování. K přístupu k nezpracovaným pixelovým datům potřebujete rozhraní COM IMemoryBufferByteAccess. Přidejte tuto definici do projektu:

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

Pak zpracujte rámce:

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

Blok nebezpečného kódu používá IMemoryBufferByteAccess k získání přímého ukazatele na rastrová data. V nastavení projektu je potřeba povolit nebezpečný kód tak, že ho přidáte <AllowUnsafeBlocks>true</AllowUnsafeBlocks>.csproj do souboru.

Použití operací OpenCV

Můžete použít jakoukoli operaci zpracování obrazu OpenCV na Mat. Tady je příklad, který používá detekci Canny Edge:

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

Image Přidejte do XAML ovládací prvek pro zobrazení zpracovaných snímků:

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

Vyčistěte zdroje

Až budete hotovi, zastavte čtečku snímků a uvolněte prostředky:

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

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