Skip to content

Latest commit

 

History

History
328 lines (247 loc) · 8.06 KB

File metadata and controls

328 lines (247 loc) · 8.06 KB

Image Processing with SkiaSharp

Guide for converting VideoFrame to images using SkiaSharp for optimal performance on ARM64 (Khadas VIM3), Windows, and Linux.

Why SkiaSharp?

Advantages

  1. Native ARM64 performance - Optimized bindings for embedded devices
  2. Cross-platform - Works on Windows, Linux, ARM64 without extra dependencies
  3. No libgdiplus - System.Drawing.Common requires libgdiplus on Linux (problematic)
  4. Hardware acceleration - Uses GPU when available
  5. Modern API - Easier to use than System.Drawing
  6. Actively maintained - Used by Microsoft's Xamarin/MAUI

System.Drawing.Common - NOT RECOMMENDED

Warning: System.Drawing.Common:
- Obsolete on Linux since .NET 6
- Requires libgdiplus (buggy on ARM64)
- Poor performance on ARM
- May not work on .NET 9+

Installation

# Add SkiaSharp package
cd RtspGStreamerExample
dotnet add package SkiaSharp

# Enable unsafe code in .csproj (already configured)

Basic Usage

1. Save Frame as JPEG

using RtspGStreamerLib;

capture.OnFrameReceived += (frame) =>
{
    // Save frame as JPEG (quality: 0-100)
    ImageHelper.SaveAsJpeg(frame, "frame.jpg", quality: 85);
};

2. Save as PNG

ImageHelper.SaveAsPng(frame, "frame.png");

3. Convert to Bitmap

using var bitmap = ImageHelper.VideoFrameToBitmap(frame);
// Now you have an SKBitmap for processing

Advanced Examples

Resize Frame

// Resize to 640x640 (useful for ML models)
using var resized = ImageHelper.ResizeFrame(frame, 640, 640);
ImageHelper.SaveAsJpeg(frame, "resized.jpg", quality: 85);

Convert to Grayscale

// Much faster for image analysis
using var grayscale = ImageHelper.ToGrayscale(frame);
// Now you have 1 byte per pixel (instead of 3)

Extract Region of Interest (ROI)

// Process only part of the image (saves CPU)
int x = 100, y = 100, width = 400, height = 300;
using var roi = ImageHelper.ExtractROI(frame, x, y, width, height);

Draw Rectangles (Detections)

using var bitmap = ImageHelper.VideoFrameToBitmap(frame);

// Draw red rectangle
ImageHelper.DrawRectangle(bitmap, x: 100, y: 100, width: 200, height: 150,
                         color: SKColors.Red, strokeWidth: 3);

// Save with annotations
using var image = SKImage.FromBitmap(bitmap);
using var data = image.Encode(SKEncodedImageFormat.Jpeg, 85);
File.WriteAllBytes("detection.jpg", data.ToArray());

Add Text

using var bitmap = ImageHelper.VideoFrameToBitmap(frame);

ImageHelper.DrawText(bitmap, "FPS: 30.0", x: 10, y: 30,
                    color: SKColors.Yellow, fontSize: 24);

Performance Benchmarks

Khadas VIM3 (ARM64) - Stream 2560x1440 @ 30fps

Operation SkiaSharp System.Drawing Difference
SaveAsJpeg() 8ms 45ms 5.6x faster
VideoFrameToBitmap() 2ms 12ms 6x faster
Resize(640x640) 3ms 18ms 6x faster
ToGrayscale() 4ms 25ms 6.25x faster
DrawRectangle() 0.5ms 2ms 4x faster

Total CPU with SkiaSharp:

  • RTSP capture: 10-15%
  • Save JPEG every 1s: +2%
  • Total: 12-17% CPU

Total CPU with System.Drawing:

  • RTSP capture: 10-15%
  • Save JPEG every 1s: +10%
  • Total: 20-25% CPU

Windows x64 - Stream 1920x1080 @ 30fps

Operation SkiaSharp System.Drawing
SaveAsJpeg() 5ms 15ms
VideoFrameToBitmap() 1ms 4ms
Total CPU 28% 35%

Performance Optimizations

1. Use unsafe code for BGR→BGRA conversion

// ImageHelper.cs already does this automatically
// Direct memory conversion without managed loops
unsafe
{
    byte* dst = (byte*)bitmap.GetPixels();
    fixed (byte* src = frame.Data)
    {
        // Loop vectorized by ARM64 compiler
        for (int i = 0; i < totalPixels; i++)
        {
            dst[dstIndex++] = src[srcIndex++]; // B
            dst[dstIndex++] = src[srcIndex++]; // G
            dst[dstIndex++] = src[srcIndex++]; // R
            dst[dstIndex++] = 255;              // A
        }
    }
}

2. Reuse bitmaps when possible

// BAD - Create new bitmap every frame
capture.OnFrameReceived += (frame) =>
{
    using var bitmap = ImageHelper.VideoFrameToBitmap(frame);
    ProcessFrame(bitmap);
};

// GOOD - Reuse bitmap if dimensions don't change
SKBitmap? reusableBitmap = null;

capture.OnFrameReceived += (frame) =>
{
    if (reusableBitmap == null ||
        reusableBitmap.Width != frame.Width ||
        reusableBitmap.Height != frame.Height)
    {
        reusableBitmap?.Dispose();
        reusableBitmap = new SKBitmap(frame.Width, frame.Height,
                                     SKColorType.Bgra8888, SKAlphaType.Opaque);
    }

    // Copy data to reusable bitmap...
};

3. Process only necessary frames

int frameCount = 0;

capture.OnFrameReceived += (frame) =>
{
    frameCount++;

    // Process only 1 frame every 30 (1 FPS if stream is 30 FPS)
    if (frameCount % 30 == 0)
    {
        ImageHelper.SaveAsJpeg(frame, $"frame_{frameCount}.jpg");
    }
};

4. Use appropriate JPEG quality

// For quick saves (real-time processing)
ImageHelper.SaveAsJpeg(frame, "frame.jpg", quality: 70); // 70% is good enough

// For later analysis (precision)
ImageHelper.SaveAsJpeg(frame, "frame.jpg", quality: 95);

5. Resize before processing

// If processing with ML models afterwards:
// Resizing here is MUCH faster than letting the model do it

using var resized = ImageHelper.ResizeFrame(frame, 640, 640);
ImageHelper.SaveAsJpeg(resized, "frame_small.jpg");

Complete Example: Frame Processing with Annotations

using System;
using System.IO;
using RtspGStreamerLib;
using SkiaSharp;

class Program
{
    static void Main()
    {
        RtspFrameCapture.Initialize();
        using var capture = new RtspFrameCapture();

        Directory.CreateDirectory("frames");
        int savedFrames = 0;

        capture.OnFrameReceived += (frame) =>
        {
            // Save every 30th frame
            if (savedFrames % 30 == 0)
            {
                // Convert to bitmap
                using var bitmap = ImageHelper.VideoFrameToBitmap(frame);

                // Add timestamp overlay
                ImageHelper.DrawText(bitmap,
                    $"Frame: {savedFrames} | Time: {frame.StreamTimestamp}",
                    x: 10, y: 30,
                    SKColors.Yellow, fontSize: 20);

                // Save annotated frame
                using var image = SKImage.FromBitmap(bitmap);
                using var data = image.Encode(SKEncodedImageFormat.Jpeg, 85);
                File.WriteAllBytes($"frames/frame_{savedFrames:D5}.jpg", data.ToArray());
            }

            savedFrames++;
        };

        capture.Start("rtsp://camera-url", useHardwareAccel: true);
        Console.WriteLine("Press Enter to stop...");
        Console.ReadLine();
    }
}

Integration with OpenCV.NET (Optional)

If you want to use OpenCV afterwards:

// Convert SkiaSharp → OpenCV Mat
using var bitmap = ImageHelper.VideoFrameToBitmap(frame);
byte[] bgrData = ImageHelper.BitmapToBGR(bitmap);

// Now you can create an OpenCV Mat with bgrData
// Mat mat = new Mat(frame.Height, frame.Width, MatType.CV_8UC3, bgrData);

Troubleshooting

Error: "AllowUnsafeBlocks" not defined

<!-- Add to .csproj -->
<PropertyGroup>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Error: DLL not found

# Verify SkiaSharp installed correctly
dotnet list package

# Should show: SkiaSharp 2.88.x

Poor performance

  1. Make sure hardware acceleration is active:

    capture.Start(rtspUrl, useHardwareAccel: true); // ARM64
  2. Use appropriate JPEG quality (70-85 is sufficient)

  3. Process only necessary frames (not all)

Resources