Używanie biblioteki OpenCV z użyciem MediaFrameReader

W tym artykule pokazano, jak używać biblioteki OpenCV (open source przetwarzanie obrazów) z elementem MediaFrameReader do przetwarzania ramek aparatu w czasie rzeczywistym. Ta technika jest przydatna w zadaniach związanych z analizą obrazów, takich jak wykrywanie krawędzi, śledzenie obiektów i odejmowanie tła w aplikacji klasycznej WinUI 3.

Wymagania wstępne

Przed rozpoczęciem upewnij się, że masz następujące elementy:

  • Projekt aplikacji klasycznej WinUI 3 (pakietowany lub niepakietowany).
  • Zgodna kamera internetowa podłączona do urządzenia.
  • Pakiety NuGet OpenCvSharp4 i OpenCvSharp4.runtime.win zainstalowane w projekcie.
  • Dostęp do aparatu zadeklarowany dla aplikacji zgodnie z opisem w temacie Deklarowanie dostępu do aparatu.

Aby zainstalować program OpenCV dla .NET:

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

Deklarowanie dostępu do aparatu

Zanim aplikacja będzie mogła używać elementu MediaFrameReader do odczytywania klatek z kamerki internetowej, musisz zadeklarować, że aplikacja korzysta z kamery. Jak to zrobić, zależy od tego, czy aplikacja jest spakowana.

  • Aplikacje pakietowe: Dodaj webcam możliwości urządzenia do Package.appxmanifest:

    <Capabilities>
        <DeviceCapability Name="webcam" />
    </Capabilities>
    
  • Aplikacje bez pakietu: Aplikacje bez pakietu nie mają manifestu pakietu, więc nie ma żadnych możliwości do zadeklarowania. Dostęp do aparatu dla aplikacji niespakietowanych jest natomiast kontrolowany przez ustawienie Zezwalaj aplikacjom klasycznym na dostęp do aparatu w obszarze Ustawienia>Prywatność i zabezpieczenia>Aparat na urządzeniu użytkownika.

W obu przypadkach użytkownik może nadal odmawiać dostępu do aparatu na poziomie systemu operacyjnego. Sprawdź dostęp przed zainicjowaniem kamery i odpowiednio obsłuż przypadek odmowy dostępu, zgodnie z opisem w Obsługa ustawień prywatności kamery w systemie Windows.

Skonfiguruj MediaFrameReader

Najpierw utwórz instancję MediaCapture i znajdź źródło kolorowej klatki wideo. Następnie utwórz MediaFrameReader, aby odbierać ramki:

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

Ważna

Ustaw MemoryPreference na Cpu, aby ramki były dostarczane jako obiekty SoftwareBitmap, do których można uzyskać dostęp bezpośrednio z kodu zarządzanego.

Przetwarzanie ramek za pomocą openCV

W procedurze obsługi zdarzeń FrameArrived pobierz element SoftwareBitmap z ramki i przekonwertuj go na openCV Mat na potrzeby przetwarzania. Aby uzyskać dostęp do nieprzetworzonych danych pikseli, potrzebny jest interfejs IMemoryBufferByteAccess COM. Dodaj tę definicję 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);
}

Następnie przetwórz ramki:

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

Niebezpieczny blok kodu używa funkcji IMemoryBufferByteAccess , aby uzyskać bezpośredni wskaźnik do danych mapy bitowej. Musisz włączyć niebezpieczny kod w ustawieniach projektu, dodając element <AllowUnsafeBlocks>true</AllowUnsafeBlocks> do pliku .csproj.

Stosowanie operacji OpenCV

Możesz zastosować dowolną operację przetwarzania obrazu OpenCV do Mat. Oto przykład, który stosuje wykrywanie krawędzi 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;
    });
}

Dodaj kontrolkę Image do kodu XAML, aby wyświetlić przetworzone ramki:

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

Uprzątnij zasoby

Zatrzymaj czytnik ramek i zwolnij zasoby po zakończeniu:

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

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