Webreaper
Webreaper

Reputation: 603

How to convert an ImageSharp Image<> to a SkiaSharp SkImage?

I've got an ImageSharp Image<Rgb24> image which is already loaded in memory, and I need to convert it into a SkiaSharp SkImage object to pass into a library that I'm using. I'd like to use the already-loaded bytes to save hitting the disk again unnecessarily. If I can re-use the byte/pixel data directly that would be awesome, but I'm fine if I have to temporarily copy the bytes to create the SkImage too.

I've tried a few things such as the following, but I'm flailing around a bit and not sure if this is the right approach:

public Task DoThingsWithImage(Image<Rgb24> imageSharpImage)
{
    byte[] pixelBytes = new byte[imageSharpImage.Width * imageSharpImage.Height * Unsafe.SizeOf<Rgba32>()];
    imageSharpImage.CopyPixelDataTo((pixelBytes));
    var skImage = SKImage.FromEncodedData(pixelBytes);

    // Do something with skImage here
}
    

Alternatively, if it's easier to go the other way - i.e., load the image as an SkImage and then convert it to an ImageSharp Image<Rgb24> that would work too. My app is processing a pipeline of operations, some of which use ImageSharp, and some of which use SkiaSharp, so it doesn't matter much which I use to load the image from disk.

I'm using .Net 9 and the latest versions of ImageSharp and SkiaSharp.

Thanks!

Upvotes: 1

Views: 202

Answers (2)

Webreaper
Webreaper

Reputation: 603

So to add to Rook's excellent answer above, I was able to generate an SkImage by pixel-copying, which achieved what I needed too.

    private SKImage CreateSkImageFromImageSharp(Image<Rgb24> imageSharpImage)
    {
        byte[] buffer = new byte[imageSharpImage.Width * imageSharpImage.Height * 4];

        int pixel = 0;
        // First, convert from an image, to an array of RGB float values. 
        imageSharpImage.ProcessPixelRows(pixelAccessor =>
        {
            for ( var y = 0; y < pixelAccessor.Height; y++ )
            {
                var row = pixelAccessor.GetRowSpan(y);
                for(var x = 0; x < pixelAccessor.Width; x++ )
                { 
                    buffer[pixel * 4 + 0] = row[x].R;
                    buffer[pixel * 4 + 1] = row[x].G;
                    buffer[pixel * 4 + 2] = row[x].B;
                    buffer[pixel * 4 + 3] = 255; // Alpha
                    pixel++;
                }
            }
        });
        var image = SKImage.FromPixelCopy(new SKImageInfo(imageSharpImage.Width, imageSharpImage.Height, SKColorType.Rgb888x), buffer);

        return image;
    }

Upvotes: 1

Rook
Rook

Reputation: 6145

Firstly and most importantly: SkiaSharp does not support 24BPP RGB. You need to convert your 24bpp image to 32bpp format first, and that will almost inevitably require allocation and copying.

If you are resigned to allocation and copying anyway, things become a little simpler. You should construct an SkBitmap instance, because those can be writeable whereas SkImage is not. You can then trivially call SkImage.FromBitmap to create your image.

As copying or converting rows and pixels isn't what you asked about, and as it doesn't require understanding the not entirely well documented memory management of the two libraries, I won't talk about it further here.


If on the other hand you're prepared to work with eg. Image<Rgba32>, you can wrap the buffer underlying an Image<T> with an SkImage without any copying. The stuff you want is probably documented here: https://docs.sixlabors.com/articles/imagesharp/memorymanagement.html

To highlight the important bits of that doc:

You need to ensure that the image is loaded into contiguous memory, which imagesharp will not always do for you by default.

Configuration customConfig = Configuration.Default.Clone();
customConfig.PreferContiguousImageBuffers = true;

using (Image<Rgba32> image = new(customConfig, 640, 480))
{
   // ...
}

You can only get a reference to the entire underlying buffer (which is what you need) if that buffer is continuous, so don't skip that step.

// this can fail, check the return value
// `image` must remain valid for the lifetime of this Memory<T>
image.DangerousTryGetSinglePixelMemory(out Memory<Rgba32> memory)

// this memory handle must remain valid for the lifetime of the
// SkImage that will be wrapping your memory.
var pin = memory.Pin();

SkImage skImage;

// you should probably catch exceptions and unpin the memory
unsafe
{
    void* rawPtr = pin.Pointer;
    var ptr = new IntPtr(rawPtr);
    var info = new SkImageInfo(image.Width, image.Height, SkColorType.Rgba8888);

    // imagesharp makes it frustratingly difficult to access stride
    // information, even though it must know it internally, but
    // for 32bpp formats the stride is generally the same as the 
    // row length in bytes.
    var stride = image.Width * 4;
    var pixmap = new SKPixmap(info, ptr, stride);

    // when this image is disposed, it will automatically unpin
    // the underlying memory
    skImage = SKImage.FromPixels(
        pixmap,
        (ptr, ctx) => pin.Dispose());
}

There are multiple ways of doing this stuff (eg. via SkiBitmap.SetPixels or SkBitmap.InstallPixels, or using SkImage.FromPixels but without a cleanup delegate, perhaps because pin is a using variable, etc etc), but this is one I use (albeit not with ImageSharp).

Upvotes: 3

Related Questions