Browse Source

Add benchmark for decoding exr image

pull/3096/head
Brian Popow 1 month ago
parent
commit
d29fe89a27
  1. 2
      src/ImageSharp/Configuration.cs
  2. 2
      src/ImageSharp/Formats/Exr/ExrDecoder.cs
  3. 126
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  4. 5
      src/ImageSharp/Formats/Exr/ExrMetadata.cs
  5. 1
      src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs
  6. 68
      tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs
  7. 1
      tests/ImageSharp.Tests/TestImages.cs
  8. 3
      tests/Images/Input/Exr/Calliphora_benchmark.exr

2
src/ImageSharp/Configuration.cs

@ -5,10 +5,10 @@ using System.Collections.Concurrent;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;

2
src/ImageSharp/Formats/Exr/ExrDecoder.cs

@ -28,7 +28,7 @@ public class ExrDecoder : ImageDecoder
return new ExrDecoderCore(new ExrDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken);
}
//// <inheritdoc/>
/// <inheritdoc/>
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));

126
src/ImageSharp/Formats/Exr/ExrDecoderCore.cs

@ -6,7 +6,6 @@ using System.Buffers;
using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Text;
using SixLabors.ImageSharp.Formats.Exr.Compression;
using SixLabors.ImageSharp.Formats.Exr.Constants;
@ -23,7 +22,6 @@ namespace SixLabors.ImageSharp.Formats.Exr;
internal sealed class ExrDecoderCore : ImageDecoderCore
{
private const float Scale32Bit = 1f / 0xFFFFFFFF;
private static readonly Vector4 Scale32BitVector = Vector128.Create(Scale32Bit, Scale32Bit, Scale32Bit, 1f).AsVector4();
/// <summary>
/// Reusable buffer.
@ -56,11 +54,6 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
this.memoryAllocator = this.configuration.MemoryAllocator;
}
/// <summary>
/// Gets the dimensions of the image.
/// </summary>
public Size Dimensions => new(this.Width, this.Height);
/// <summary>
/// Gets or sets the image width.
/// </summary>
@ -81,16 +74,6 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
/// </summary>
private ExrCompression Compression { get; set; }
/// <summary>
/// Gets or sets the image data type, either RGB, RGBA or gray.
/// </summary>
private ExrImageDataType ImageDataType { get; set; }
/// <summary>
/// Gets or sets the image type, either ScanLine or tiled.
/// </summary>
private ExrImageType ImageType { get; set; }
/// <summary>
/// Gets or sets the header attributes.
/// </summary>
@ -106,7 +89,6 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
}
ExrPixelType pixelType = this.ValidateChannels();
this.ReadImageDataType();
Image<TPixel> image = new(this.configuration, this.Width, this.Height, this.metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
@ -226,7 +208,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++)
{
ExrChannelInfo channel = this.Channels[channelIdx];
offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData.Slice(offset), redPixelData, greenPixelData, bluePixelData, alphaPixelData, width);
offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData[offset..], redPixelData, greenPixelData, bluePixelData, alphaPixelData, width);
}
for (int x = 0; x < width; x++)
@ -320,29 +302,18 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
}
}
private static int ReadChannelData(ExrChannelInfo channel, Span<byte> decompressedPixelData, Span<float> pixelData, int width)
private static int ReadChannelData(ExrChannelInfo channel, Span<byte> decompressedPixelData, Span<float> pixelData, int width) => channel.PixelType switch
{
switch (channel.PixelType)
{
case ExrPixelType.Half:
return ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width);
case ExrPixelType.Float:
return ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width);
}
return 0;
}
ExrPixelType.Half => ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width),
ExrPixelType.Float => ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width),
_ => 0,
};
private static int ReadChannelData(ExrChannelInfo channel, Span<byte> decompressedPixelData, Span<uint> pixelData, int width)
private static int ReadChannelData(ExrChannelInfo channel, Span<byte> decompressedPixelData, Span<uint> pixelData, int width) => channel.PixelType switch
{
switch (channel.PixelType)
{
case ExrPixelType.UnsignedInt:
return ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width);
}
return 0;
}
ExrPixelType.UnsignedInt => ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width),
_ => 0,
};
private static int ReadPixelRowChannelHalfSingle(Span<byte> decompressedPixelData, Span<float> channelData, int width)
{
@ -442,7 +413,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
byte flagsByte2 = (byte)stream.ReadByte();
if ((flagsByte0 & (1 << 1)) != 0)
{
this.ImageType = ExrImageType.Tiled;
ExrThrowHelper.ThrowNotSupported("Decoding tiled exr images is not supported yet!");
}
this.HeaderAttributes = this.ParseHeaderAttributes(stream);
@ -560,7 +531,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
private List<ExrChannelInfo> ReadChannelList(BufferedReadStream stream, int attributeSize)
{
List<ExrChannelInfo> channels = new();
List<ExrChannelInfo> channels = [];
while (attributeSize > 1)
{
ExrChannelInfo channelInfo = this.ReadChannelInfo(stream, out int bytesRead);
@ -654,76 +625,11 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
return pixelType.Value;
}
private bool IsSupportedCompression()
{
switch (this.Compression)
{
case ExrCompression.None:
case ExrCompression.Zip:
case ExrCompression.Zips:
case ExrCompression.RunLengthEncoded:
case ExrCompression.B44:
return true;
}
return false;
}
private void ReadImageDataType()
private bool IsSupportedCompression() => this.Compression switch
{
bool hasRedChannel = false;
bool hasGreenChannel = false;
bool hasBlueChannel = false;
bool hasAlphaChannel = false;
bool hasLuminance = false;
foreach (ExrChannelInfo channelInfo in this.Channels)
{
if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal))
{
hasAlphaChannel = true;
}
if (channelInfo.ChannelName.Equals("R", StringComparison.Ordinal))
{
hasRedChannel = true;
}
if (channelInfo.ChannelName.Equals("G", StringComparison.Ordinal))
{
hasGreenChannel = true;
}
if (channelInfo.ChannelName.Equals("B", StringComparison.Ordinal))
{
hasBlueChannel = true;
}
if (channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal))
{
hasLuminance = true;
}
}
if (hasRedChannel && hasGreenChannel && hasBlueChannel && hasAlphaChannel)
{
this.ImageDataType = ExrImageDataType.Rgba;
return;
}
if (hasRedChannel && hasGreenChannel && hasBlueChannel)
{
this.ImageDataType = ExrImageDataType.Rgb;
return;
}
if (hasLuminance && this.Channels.Count == 1)
{
this.ImageDataType = ExrImageDataType.Gray;
return;
}
ExrThrowHelper.ThrowNotSupported("The image contains channels, which are not supported!");
}
ExrCompression.None or ExrCompression.Zip or ExrCompression.Zips or ExrCompression.RunLengthEncoded or ExrCompression.B44 => true,
_ => false,
};
private bool HasAlpha()
{

5
src/ImageSharp/Formats/Exr/ExrMetadata.cs

@ -30,17 +30,22 @@ public class ExrMetadata : IFormatMetadata<ExrMetadata>
/// </summary>
public ExrPixelType PixelType { get; set; } = ExrPixelType.Float;
/// <inheritdoc/>
public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException();
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel> => throw new NotImplementedException();
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new ExrMetadata(this);
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException();
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException();
/// <inheritdoc/>
ExrMetadata IDeepCloneable<ExrMetadata>.DeepClone() => throw new NotImplementedException();
}

1
src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs

@ -318,6 +318,7 @@ public partial struct Rgba32 : IPixel<Rgba32>, IPackedVector<uint>
/// Initializes the pixel instance from an <see cref="Rgb96"/> value.
/// </summary>
/// <param name="source">The <see cref="Rgb96"/> value.</param>
/// <returns>The pixel value as Rgba32.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rgba32 FromRgb96(Rgb96 source)
=> new()

68
tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs

@ -0,0 +1,68 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes;
using ImageMagick;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests;
namespace SixLabors.ImageSharp.Benchmarks.Codecs;
[MarkdownExporter]
[HtmlExporter]
[Config(typeof(Config.Short))]
public class DecodeExr
{
private Configuration configuration;
private byte[] imageBytes;
private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage);
[Params(TestImages.Exr.Benchamrk)]
public string TestImage { get; set; }
[GlobalSetup]
public void ReadImages()
{
this.configuration = Configuration.CreateDefaultInstance();
new ExrConfigurationModule().Configure(this.configuration);
this.imageBytes ??= File.ReadAllBytes(this.TestImageFullPath);
}
[Benchmark(Description = "Magick Exr")]
public int ExrImageMagick()
{
MagickReadSettings settings = new() { Format = MagickFormat.Exr };
using MemoryStream memoryStream = new(this.imageBytes);
using MagickImage image = new(memoryStream, settings);
return image.Width;
}
[Benchmark(Description = "ImageSharp Exr")]
public int ExrImageSharp()
{
using MemoryStream memoryStream = new(this.imageBytes);
using Image<Rgba32> image = Image.Load<Rgba32>(memoryStream);
return image.Height;
}
/* Results 27.03.2026
BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8037/25H2/2025Update/HudsonValley2)
Intel Core i7-14700T 1.30GHz, 1 CPU, 28 logical and 20 physical cores
.NET SDK 10.0.201
[Host] : .NET 8.0.25 (8.0.25, 8.0.2526.11203), X64 RyuJIT x86-64-v3
Job-VDWIGO : .NET 8.0.25 (8.0.25, 8.0.2526.11203), X64 RyuJIT x86-64-v3
Runtime=.NET 8.0 Arguments=/p:DebugType=portable IterationCount=3
LaunchCount=1 WarmupCount=3
| Method | TestImage | Mean | Error | StdDev | Allocated |
|----------------- |----------------------------- |---------:|---------:|---------:|----------:|
| 'Magick Exr' | Exr/Calliphora_benchmark.exr | 20.37 ms | 0.790 ms | 0.043 ms | 12.98 KB |
| 'ImageSharp Exr' | Exr/Calliphora_benchmark.exr | 45.68 ms | 4.999 ms | 0.274 ms | 34.09 KB |
*/
}

1
tests/ImageSharp.Tests/TestImages.cs

@ -1383,6 +1383,7 @@ public static class TestImages
public static class Exr
{
public const string Benchamrk = "Exr/Calliphora_benchmark.exr";
public const string Uncompressed = "Exr/Calliphora_uncompressed.exr";
public const string UncompressedFloatRgb = "Exr/rgb_float32_uncompressed.exr";
public const string UncompressedUintRgb = "Exr/rgb_uint32_uncompressed.exr";

3
tests/Images/Input/Exr/Calliphora_benchmark.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e3368860692927e709365f2d37b92411068e77a0f23624ff57af6089ec69f357
size 2592888
Loading…
Cancel
Save