Menggunakan OpenCV dengan MediaFrameReader

Artikel ini memperlihatkan cara menggunakan pustaka OpenCV (Open Source Computer Vision) dengan MediaFrameReader untuk memproses bingkai kamera secara real time. Teknik ini berguna untuk tugas analisis gambar seperti deteksi tepi, pelacakan objek, dan pengurangan latar belakang dalam aplikasi desktop WinUI 3.

Prasyarat

Sebelum mulai, pastikan bahwa Anda memiliki:

  • Proyek desktop WinUI 3 (dikemas atau tidak dikemas).
  • Webcam yang kompatibel tersambung ke perangkat Anda.
  • Paket OpenCvSharp4 dan OpenCvSharp4.runtime.win NuGet diinstal dalam proyek Anda.
  • Akses kamera dideklarasikan untuk aplikasi Anda, seperti yang dijelaskan dalam Mendeklarasikan akses kamera.

Untuk menginstal OpenCV untuk .NET:

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

Mendeklarasikan akses kamera

Sebelum aplikasi dapat menggunakan MediaFrameReader untuk membaca bingkai dari webcam, Anda harus menyatakan bahwa aplikasi Anda menggunakan kamera. Cara Anda melakukan ini tergantung pada apakah aplikasi Anda dipaketkan.

  • Aplikasi paketwebcam: Tambahkan kemampuan perangkat ke Package.appxmanifest:

    <Capabilities>
        <DeviceCapability Name="webcam" />
    </Capabilities>
    
  • Aplikasi yang tidak dikemas: Aplikasi yang tidak dikemas tidak memiliki manifes paket, sehingga tidak ada kemampuan untuk mendeklarasikan. Akses kamera untuk aplikasi yang tidak dikemas sebaliknya dikontrol oleh pengaturan Izinkan aplikasi desktop mengakses kamera Anda di bawah Pengaturan>Privasi & Kamera keamanan> pada perangkat pengguna.

Dalam kedua kasus, pengguna masih dapat menolak akses kamera di tingkat OS. Periksa akses sebelum Anda menginisialisasi kamera dan menangani kasus yang ditolak dengan baik, seperti yang dijelaskan dalam Menangani pengaturan privasi kamera Windows.

Menyiapkan MediaFrameReader

Pertama, buat MediaCapture instans dan temukan sumber bingkai video warna. Lalu buat MediaFrameReader untuk menerima frame:

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

Atur MemoryPreference ke Cpu sehingga bingkai tiba sebagai SoftwareBitmap objek yang dapat Anda akses langsung dari kode terkelola.

Bingkai proses dengan OpenCV

Di penangan peristiwa FrameArrived, ambil SoftwareBitmap dari frame dan ubah menjadi OpenCV Mat untuk diproses. Anda memerlukan antarmuka IMemoryBufferByteAccess COM untuk mengakses data piksel mentah. Tambahkan definisi ini ke proyek Anda:

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

Kemudian proses bingkai:

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 kode yang tidak aman menggunakan IMemoryBufferByteAccess untuk mendapatkan penunjuk langsung ke data bitmap. Anda perlu mengaktifkan kode tidak aman di pengaturan proyek Anda dengan menambahkan <AllowUnsafeBlocks>true</AllowUnsafeBlocks> ke file Anda .csproj .

Menerapkan operasi OpenCV

Anda dapat menerapkan operasi pemrosesan gambar OpenCV apa pun ke Mat. Berikut adalah contoh yang menerapkan deteksi tepi 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;
    });
}

Image Tambahkan kontrol ke XAML Anda untuk menampilkan bingkai yang diproses:

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

Membersihkan sumber daya

Hentikan pembaca bingkai dan lepaskan sumber daya setelah selesai:

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

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