Last active
June 27, 2025 06:15
-
-
Save stivio00/ab4d464bd1a8c9db7e81ecac940b8f11 to your computer and use it in GitHub Desktop.
Image decoding and cropping benchmarks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
// * 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
// * 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