Table of Contents

Working with Pixel Buffers

When you first start with ImageSharp, the indexer is often enough. As soon as performance, reuse across pixel formats, or interop enter the picture, it helps to know the other buffer-access patterns the library offers and why they exist.

This page is the map for that lower-level work.

Pixel Buffers Are Decoded Image Data

Pixel-buffer APIs expose the decoded raster grid in memory. They do not expose the original file bytes and they do not preserve format-specific packing such as JPEG blocks, PNG filters, GIF color-table indexes, or TIFF strip layout. By the time you are working with row spans, ImageSharp has decoded the source into the chosen TPixel representation.

Rows are addressed by image coordinates: y selects a row and x selects a pixel within that row. The APIs are row-oriented because image memory is optimized for scanning contiguous rows, even when a large image is backed by several internal buffers instead of one single allocation.

Choose the Right Access Pattern

Use:

  • indexers for occasional pixel reads or writes;
  • ProcessPixelRows(...) for fast row-by-row work in a known TPixel;
  • ProcessPixelRowsAsVector4(...) for reusable pixel-format-agnostic processing;
  • CopyPixelDataTo(...), LoadPixelData(...), and WrapMemory(...) when exchanging raw data with other systems.

Use Indexers for Simple Cases

If you only need to touch a few pixels, the indexer is the simplest option:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

using Image<Rgba32> image = new(400, 400);
image[200, 200] = Rgba32.White;

That is fine for small amounts of work, but repeated random pixel access has more overhead than processing full rows.

Use ProcessPixelRows(...) for Fast Known-Format Access

Image<TPixel> and ImageFrame<TPixel> expose ProcessPixelRows(...) so you can work with row spans directly:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

using Image<Rgba32> image = Image.Load<Rgba32>("input.png");

image.ProcessPixelRows(accessor =>
{
    for (int y = 0; y < accessor.Height; y++)
    {
        Span<Rgba32> row = accessor.GetRowSpan(y);

        for (int x = 0; x < row.Length; x++)
        {
            ref Rgba32 pixel = ref row[x];
            if (pixel.A == 0)
            {
                pixel = Rgba32.Transparent;
            }
        }
    }
});

This is the usual replacement for LockBits-style workflows when your algorithm already knows the working pixel format.

Process Multiple Images Row by Row

There are overloads for processing multiple images together:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

using Image<Rgba32> source = Image.Load<Rgba32>("source.png");
using Image<Rgba32> target = new(source.Width, source.Height);

source.ProcessPixelRows(target, (sourceAccessor, targetAccessor) =>
{
    for (int y = 0; y < sourceAccessor.Height; y++)
    {
        Span<Rgba32> sourceRow = sourceAccessor.GetRowSpan(y);
        Span<Rgba32> targetRow = targetAccessor.GetRowSpan(y);
        sourceRow.CopyTo(targetRow);
    }
});

This is a good fit for compositing, comparisons, or custom copy/merge logic.

Use ProcessPixelRowsAsVector4(...) for Pixel-Format-Agnostic Logic

If you want one processor that can run on many TPixel formats, use ProcessPixelRowsAsVector4(...):

using System.Numerics;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;

image.Mutate(context => context.ProcessPixelRowsAsVector4(row =>
{
    for (int x = 0; x < row.Length; x++)
    {
        row[x] = Vector4.SquareRoot(row[x]);
    }
}));

This is extremely useful for reusable processing logic, but remember that it introduces conversion work to and from Vector4. It is often a great tradeoff for flexibility, but it is not always the fastest possible path for a hot server-side workload.

Convert to a Working Pixel Format

Sometimes the cleanest approach is to convert the image into a known working format first:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

using Image source = Image.Load("input.tiff");
using Image<Rgba32> working = source.CloneAs<Rgba32>();

CloneAs<TPixel>() is especially useful when you want to standardize a pipeline on Rgba32, Bgra32, or another specific working format.

Copy Raw Pixels In and Out

Use CopyPixelDataTo(...) when you need a flattened copy of the root frame pixel buffer:

using SixLabors.ImageSharp.PixelFormats;

Rgba32[] pixels = new Rgba32[image.Width * image.Height];
image.CopyPixelDataTo(pixels);

Use LoadPixelData(...) when you want ImageSharp to create an owned image from raw input:

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

byte[] rgba = GetRgbaBytes();
using Image<Rgba32> image = Image.LoadPixelData<Rgba32>(rgba, width, height);

There are stride-aware overloads for both pixel and byte input. For zero-copy interop, see Interop and Raw Memory.

Span<T> Rules Still Apply

The row spans you get from pixel accessors are Span<T> values. That means they are stack-only:

  • They cannot be stored on the heap.
  • They cannot cross await boundaries.
  • They cannot be captured and used after the callback returns.

Keep all row work inside the callback that received the accessor.

Practical Guidance

  • Prefer row access over per-pixel indexers for non-trivial work.
  • Keep span usage inside the callback that supplied the row accessor.
  • Use ProcessPixelRowsAsVector4(...) when logic should be pixel-format agnostic.
  • Convert to a known working pixel format when the algorithm benefits from simpler direct access.