Skip to content

Instantly share code, notes, and snippets.

@stivio00
Last active June 27, 2025 06:15
Show Gist options
  • Select an option

  • Save stivio00/ab4d464bd1a8c9db7e81ecac940b8f11 to your computer and use it in GitHub Desktop.

Select an option

Save stivio00/ab4d464bd1a8c9db7e81ecac940b8f11 to your computer and use it in GitHub Desktop.
Image decoding and cropping benchmarks
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using ImageMagick;
using OpenCvSharp;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SkiaSharp;
namespace ImageProcessingBenchmark;
// use me like: dotnet run -c Release -- --filter * --category Decoding
// BenchmarkRunner.Run<ImageBenchmark>();
// images from https://picsum.photos/1000
[MemoryDiagnoser]
[ThreadingDiagnoser]
//[DisassemblyDiagnoser]
//[NativeMemoryProfiler]
//[InliningDiagnoser(true,true)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class ImageBenchmark
{
//[Params(1, 2, 4, 8)]
// public int DegreeOfParallelism { get; set; }
#region Init
private readonly string[] _imagePaths =
[
"images/image1.jpg",
"images/image2.jpg",
"images/image3.jpg",
"images/image4.jpg",
"images/image5.jpg"
];
private Mat[] _images;
private MagickImage[] _magickImages;
private SKBitmap[] _skiaImages;
private Image<Rgba32>[] _imageSharpImages;
[GlobalSetup]
public void Setup()
{
_images = _imagePaths.Select(p => new Mat(p)).ToArray();
_magickImages = _imagePaths.Select(p => new MagickImage(p)).ToArray();
_skiaImages = _imagePaths.Select(SKBitmap.Decode).ToArray();
_imageSharpImages = _imagePaths.Select(Image.Load<Rgba32>).ToArray();
}
#endregion
#region Decoding
[Benchmark]
[BenchmarkCategory("Decoding")]
public List<Mat> Decode_OpenCvSharp()
{
var decoded = _imagePaths
.Select(p => new Mat(p))
.ToList();
// Decode JPEG file to Mat using something more
//byte[] jpegBytes = File.ReadAllBytes("input.jpg");
//Mat mat = Cv2.ImDecode(jpegBytes, ImreadModes.Color);
// Encode Mat to JPEG bytes
//byte[] encodedJpeg;
//Cv2.ImEncode(".jpg", mat, out encodedJpeg);
return decoded;
}
[Benchmark]
[BenchmarkCategory("Decoding")]
public List<MagickImage> Decode_ImageMagick()
{
var decoded = _imagePaths
.Select(p => new MagickImage(p))
.ToList();
return decoded;
}
[Benchmark]
[BenchmarkCategory("Decoding")]
public List<SKBitmap> Decode_SkiaSharp()
{
var decoded = _imagePaths
.Select(SKBitmap.Decode)
.ToList();
return decoded;
}
[Benchmark]
[BenchmarkCategory("Decoding")]
public List<Image<Rgba32>> Decode_ImageSharp()
{
var decoded = _imagePaths
.Select(p => Image.Load<Rgba32>(p))
.ToList();
return decoded;
}
#endregion
#region Encoding
[Benchmark]
[BenchmarkCategory("Encoding")]
public List<byte[]> Encode_OpenCvSharp()
{
var encoded = _images
.Select(p => p.ToBytes())
.ToList();
return encoded;
}
[Benchmark]
[BenchmarkCategory("Encoding")]
public List<byte[]> Encode_ImageMagick()
{
var encoded = _magickImages
.Select(img =>
{
using var mem = new MemoryStream();
img.Write(mem, MagickFormat.Jpeg);
return mem.ToArray();
})
.ToList();
return encoded;
}
[Benchmark]
[BenchmarkCategory("Encoding")]
public List<byte[]> Encode_SkiaSharp()
{
var encoded = _skiaImages
.Select(img =>
{
using var data = img.Encode(SKEncodedImageFormat.Jpeg, 90);
return data.ToArray();
})
.ToList();
return encoded;
}
[Benchmark]
[BenchmarkCategory("Encoding")]
public List<byte[]> Encode_ImageSharp()
{
var encoded = _imageSharpImages
.Select(img =>
{
using var mem = new MemoryStream();
img.SaveAsJpeg(mem);
return mem.ToArray();
})
.ToList();
return encoded;
}
#endregion
#region Cropping
[Benchmark]
[BenchmarkCategory("Cropping")]
public List<Mat> CropCenter_OpenCvSharp_View()
{
var results = new List<Mat>();
foreach (var img in _images)
{
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
results.Add(new Mat(img, rect));
}
return results;
}
[Benchmark]
[BenchmarkCategory("Cropping")]
public List<Mat> CropCenter_OpenCvSharp_Copy()
{
var results = new List<Mat>();
foreach (var img in _images)
{
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
results.Add(new Mat(img, rect).Clone());
}
return results;
}
[Benchmark]
[BenchmarkCategory("Cropping")]
public List<Mat> CropCenter_OpenCvSharp_SubMatCopy()
{
var results = new List<Mat>();
foreach (var img in _images)
{
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
using var subMat = img.SubMat(rect);
results.Add(subMat.Clone());
}
return results;
}
[Benchmark]
[BenchmarkCategory("Cropping")]
public List<MagickImage> CropCenter_ImageMagick()
{
var results = new List<MagickImage>();
foreach (var img in _magickImages)
{
var x = (int)img.Width / 4;
var y = (int)img.Height / 4;
var width = img.Width / 2;
var height = img.Height / 2;
var cropped = img.Clone();
cropped.Crop(new MagickGeometry(x, y, width, height));
results.Add(new MagickImage(cropped));
}
return results;
}
[Benchmark]
[BenchmarkCategory("Cropping")]
public List<SKBitmap> CropCenter_SkiaSharp()
{
var results = new List<SKBitmap>();
foreach (var img in _skiaImages)
{
var x = img.Width / 4;
var y = img.Height / 4;
var width = img.Width / 2;
var height = img.Height / 2;
using var cropped = new SKBitmap(width, height);
using (var canvas = new SKCanvas(cropped))
{
var srcRect = new SKRectI(x, y, x + width, y + height);
var destRect = new SKRectI(0, 0, width, height);
canvas.DrawBitmap(img, srcRect, destRect);
results.Add(cropped);
}
}
return results;
}
[Benchmark]
[BenchmarkCategory("Cropping")]
public List<Image<Rgba32>> CropCenter_ImageSharp()
{
var results = new List<Image<Rgba32>>();
foreach (var img in _imageSharpImages)
{
var x = img.Width / 4;
var y = img.Height / 4;
var width = img.Width / 2;
var height = img.Height / 2;
var cropped = img.Clone(ctx => ctx.Crop(new Rectangle(x, y, width, height)));
results.Add(cropped);
}
return results;
}
#endregion
#region Parallel
[Benchmark]
[BenchmarkCategory("Parallel")]
public List<Mat> CropCenter_OpenCvSharp_ParallelFor()
{
var results = new Mat[_images.Length];
Parallel.For(0, _images.Length, i =>
{
var img = _images[i];
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
results[i] = new Mat(img, rect);
});
return results.ToList();
}
[Benchmark]
[BenchmarkCategory("Parallel")]
public int CropCenter_OpenCvSharp_PLINQ()
{
var results = _images
.AsParallel()
.Select(img =>
{
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
return new Mat(img, rect);
})
.ToList();
return results.Count;
}
[Benchmark]
[BenchmarkCategory("Parallel")]
public async Task<int> CropCenter_OpenCvSharp_Async()
{
var results = new List<Mat>();
var tasks = _images.Select(async img =>
{
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
var cropped = new Mat(img, rect);
lock (results)
{
results.Add(cropped);
}
await Task.CompletedTask;
});
await Task.WhenAll(tasks);
return results.Count;
}
[Benchmark]
[BenchmarkCategory("Parallel")]
public async Task<List<Mat>> CropCenter_OpenCvSharp_Async_NoLock()
{
var tasks = _images.Select(img =>
Task.Run(() =>
{
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
return new Mat(img, rect);
})
);
var results = await Task.WhenAll(tasks);
return results.ToList();
}
[Benchmark]
[BenchmarkCategory("Parallel")]
public List<Mat> CropCenter_OpenCvSharp_ParallelFor_COPY()
{
var results = new Mat[_images.Length];
Parallel.For(0, _images.Length, i =>
{
var img = _images[i];
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
results[i] = (new Mat(img, rect)).Clone();
});
return results.ToList();
}
[Benchmark]
[BenchmarkCategory("Parallel")]
public int CropCenter_OpenCvSharp_PLINQ_COPY()
{
var results = _images
.AsParallel()
.Select(img =>
{
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
return (new Mat(img, rect)).Clone();
})
.ToList();
return results.Count;
}
[Benchmark]
[BenchmarkCategory("Parallel")]
public async Task<int> CropCenter_OpenCvSharp_Async_COPY()
{
var results = new List<Mat>();
var tasks = _images.Select(async img =>
{
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
var cropped = new Mat(img, rect);
lock (results)
{
results.Add(cropped.Clone());
}
await Task.CompletedTask;
});
await Task.WhenAll(tasks);
return results.Count;
}
[Benchmark]
[BenchmarkCategory("Parallel")]
public async Task<List<Mat>> CropCenter_OpenCvSharp_Async_NoLock_COPY()
{
var tasks = _images.Select(img =>
Task.Run(() =>
{
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
return (new Mat(img, rect)).Clone();
})
);
var results = await Task.WhenAll(tasks);
return results.ToList();
}
#endregion
#region FullProcess
// OpenCvSharp full process
[Benchmark]
[BenchmarkCategory("FullProcess")]
public List<byte[]> FullProcess_OpenCvSharp()
{
var result = new List<byte[]>();
foreach (var path in _imagePaths)
{
using var img = new Mat(path);
var rect = new Rect(img.Width / 4, img.Height / 4, img.Width / 2, img.Height / 2);
using var cropped = new Mat(img, rect);
result.Add(cropped.ToBytes());
}
return result;
}
// ImageMagick full process
[Benchmark]
[BenchmarkCategory("FullProcess")]
public List<byte[]> FullProcess_ImageMagick()
{
var result = new List<byte[]>();
foreach (var path in _imagePaths)
{
using var img = new MagickImage(path);
var x = (int)img.Width / 4;
var y = (int)img.Height / 4;
var width = img.Width / 2;
var height = img.Height / 2;
using var cropped = img.Clone();
cropped.Crop(new MagickGeometry(x, y, width, height));
using var mem = new MemoryStream();
cropped.Write(mem, MagickFormat.Jpeg);
result.Add(mem.ToArray());
}
return result;
}
// SkiaSharp full process
[Benchmark]
[BenchmarkCategory("FullProcess")]
public List<byte[]> FullProcess_SkiaSharp()
{
var result = new List<byte[]>();
foreach (var path in _imagePaths)
{
using var img = SKBitmap.Decode(path);
var x = img.Width / 4;
var y = img.Height / 4;
var width = img.Width / 2;
var height = img.Height / 2;
using var cropped = new SKBitmap(width, height);
using (var canvas = new SKCanvas(cropped))
{
var srcRect = new SKRectI(x, y, x + width, y + height);
var destRect = new SKRectI(0, 0, width, height);
canvas.DrawBitmap(img, srcRect, destRect);
}
using var data = cropped.Encode(SKEncodedImageFormat.Jpeg, 90);
result.Add(data.ToArray());
}
return result;
}
// ImageSharp full process
[Benchmark]
[BenchmarkCategory("FullProcess")]
public List<byte[]> FullProcess_ImageSharp()
{
var result = new List<byte[]>();
foreach (var path in _imagePaths)
{
using var img = Image.Load<Rgba32>(path);
var x = img.Width / 4;
var y = img.Height / 4;
var width = img.Width / 2;
var height = img.Height / 2;
using var cropped = img.Clone(ctx => ctx.Crop(new Rectangle(x, y, width, height)));
using var mem = new MemoryStream();
cropped.SaveAsJpeg(mem);
result.Add(mem.ToArray());
}
return result;
}
#endregion
//TODO: Add more benchmarks for other operations like resizing, rotating, etc.
}
@stivio00
Copy link
Copy Markdown
Author

stivio00 commented Jun 26, 2025

// * Summary *

BenchmarkDotNet v0.15.2, Windows 11 (10.0.26100.4351/24H2/2024Update/HudsonValley)
AMD Ryzen 9 6900HS with Radeon Graphics 3.30GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.300
[Host] : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2
DefaultJob : .NET 9.0.5 (9.0.525.21509), X64 RyuJIT AVX2

Method Categories Mean Error StdDev Completed Work Items Lock Contentions Gen0 Gen1 Gen2 Allocated
CropCenter_OpenCvSharp_View Cropping 1.016 us 0.0086 us 0.0080 us - - 0.0591 0.0572 - 496 B
CropCenter_OpenCvSharp_Copy Cropping 718.691 us 14.3064 us 18.0930 us - - - - - 816 B
CropCenter_OpenCvSharp_SubMatCopy Cropping 716.046 us 14.2492 us 18.5280 us - - - - - 816 B
CropCenter_ImageMagick Cropping 9,704.698 us 24.5037 us 20.4617 us - - - - - 19056 B
CropCenter_SkiaSharp Cropping 4,279.567 us 58.3512 us 51.7268 us - - - - - 1616 B
CropCenter_ImageSharp Cropping 571.734 us 6.7460 us 6.3102 us 28.6748 0.0010 5.8594 1.9531 1.9531 2556920 B
Decode_OpenCvSharp Decoding 33,739.379 us 518.3452 us 459.4995 us - - - - - 464 B
Decode_ImageMagick Decoding 65,520.181 us 760.2030 us 711.0944 us - - - - - 15224 B
Decode_SkiaSharp Decoding 50,356.675 us 546.5347 us 511.2289 us - - - - - 10264 B
Decode_ImageSharp Decoding 36,822.421 us 533.5865 us 499.1172 us - - - - - 117541 B
Encode_OpenCvSharp Encoding 122,052.356 us 893.0460 us 791.6620 us - - 200.0000 200.0000 200.0000 6534259 B
Encode_ImageMagick Encoding 35,818.595 us 195.1123 us 182.5082 us - - 428.5714 357.1429 357.1429 2045993 B
Encode_SkiaSharp Encoding 58,997.266 us 254.9138 us 238.4465 us - - 111.1111 111.1111 111.1111 726095 B
Encode_ImageSharp Encoding 19,658.088 us 84.3782 us 78.9274 us - - 468.7500 375.0000 375.0000 2163233 B
FullProcess_OpenCvSharp FullProcess 64,981.480 us 1,138.3206 us 1,217.9903 us - - 375.0000 375.0000 375.0000 1622850 B
FullProcess_ImageMagick FullProcess 86,796.775 us 1,708.5734 us 1,754.5791 us - - - - - 614747 B
FullProcess_SkiaSharp FullProcess 73,240.871 us 1,419.7512 us 1,457.9799 us - - - - - 262648 B
FullProcess_ImageSharp FullProcess 42,235.261 us 349.4506 us 309.7788 us 13.7500 - 83.3333 - - 845981 B
CropCenter_OpenCvSharp_ParallelFor Parallel 2.414 us 0.0163 us 0.0144 us 3.0231 0.0000 0.3166 0.3128 - 2637 B
CropCenter_OpenCvSharp_PLINQ Parallel 4.592 us 0.0447 us 0.0374 us 15.0000 0.0000 0.9460 0.9308 - 7928 B
CropCenter_OpenCvSharp_Async Parallel 1.239 us 0.0231 us 0.0193 us - - 0.1049 0.1030 - 880 B
CropCenter_OpenCvSharp_Async_NoLock Parallel 2.254 us 0.0319 us 0.0298 us 5.0011 0.0000 0.2098 0.2060 - 1760 B
CropCenter_OpenCvSharp_ParallelFor_COPY Parallel 349.659 us 6.8084 us 8.8529 us 6.0000 0.0015 - - - 3526 B
CropCenter_OpenCvSharp_PLINQ_COPY Parallel 469.242 us 9.2610 us 21.8294 us 15.0000 - 0.9766 0.4883 - 8312 B
CropCenter_OpenCvSharp_Async_COPY Parallel 718.345 us 14.0336 us 18.2476 us - - - - - 1200 B
CropCenter_OpenCvSharp_Async_NoLock_COPY Parallel 333.870 us 6.5263 us 8.7124 us 5.0000 0.0005 - - - 2144 B

// * Warnings *
MultimodalDistribution
ImageBenchmark.CropCenter_OpenCvSharp_Copy: Default -> It seems that the distribution is bimodal (mValue = 3.83)
ImageBenchmark.CropCenter_OpenCvSharp_SubMatCopy: Default -> It seems that the distribution is bimodal (mValue = 3.67)
ImageBenchmark.CropCenter_OpenCvSharp_Async_NoLock_COPY: Default -> It seems that the distribution can have several modes (mValue = 3)

// * Hints *
Outliers
ImageBenchmark.CropCenter_OpenCvSharp_SubMatCopy: Default -> 1 outlier was removed (805.82 us)
ImageBenchmark.CropCenter_ImageMagick: Default -> 2 outliers were removed (9.77 ms, 9.78 ms)
ImageBenchmark.CropCenter_SkiaSharp: Default -> 1 outlier was removed (5.03 ms)
ImageBenchmark.Decode_OpenCvSharp: Default -> 1 outlier was removed (35.46 ms)
ImageBenchmark.Decode_ImageMagick: Default -> 1 outlier was detected (63.81 ms)
ImageBenchmark.Encode_OpenCvSharp: Default -> 1 outlier was removed (124.08 ms)
ImageBenchmark.Encode_ImageMagick: Default -> 1 outlier was detected (35.42 ms)
ImageBenchmark.FullProcess_OpenCvSharp: Default -> 1 outlier was removed (68.39 ms)
ImageBenchmark.FullProcess_ImageSharp: Default -> 1 outlier was removed (43.57 ms)
ImageBenchmark.CropCenter_OpenCvSharp_ParallelFor: Default -> 1 outlier was removed, 2 outliers were detected (2.39 us, 2.50 us)
ImageBenchmark.CropCenter_OpenCvSharp_PLINQ: Default -> 2 outliers were removed (5.16 us, 7.79 us)
ImageBenchmark.CropCenter_OpenCvSharp_Async: Default -> 2 outliers were removed (1.32 us, 1.32 us)

// * Legends *
Categories : All categories of the corresponded method, class, and assembly
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
Completed Work Items : The number of work items that have been processed in ThreadPool (per single operation)
Lock Contentions : The number of times there was contention upon trying to take a Monitor's lock (per single operation)
Gen0 : GC Generation 0 collects per 1000 operations
Gen1 : GC Generation 1 collects per 1000 operations
Gen2 : GC Generation 2 collects per 1000 operations
Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
1 us : 1 Microsecond (0.000001 sec)

// * Diagnostic Output - ThreadingDiagnoser *

// * Diagnostic Output - MemoryDiagnoser *

// ***** BenchmarkRunner: End *****
Run time: 00:07:52 (472.17 sec), executed benchmarks: 26

Global total time: 00:07:57 (477.97 sec), executed benchmarks: 26
// * Artifacts cleanup *
Artifacts cleanup is finished

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment