mirror of https://github.com/SixLabors/ImageSharp
Browse Source
Write still images, grid tiles, and sequence frames into their final regions, combining presentation transforms, alpha, and ICC conversion. Add explicit chroma upsampling selection and correct high-bit-depth sample normalization. Remove JPEG item handling and restore shared Flip/Rotate implementations. Update encoded versus decoded metadata assertions and reference-image output. Preserve the existing allocator identity contract. Document native reference provenance, accepted ICC baselines, and remaining encoder acceptance gaps. Validation: Roslynk reports zero errors; Release/net11 builds successfully. Visual Studio VSTest passed all 10,487 selected HEIF/AV1, ICC, Flip/Rotate, and allocator cases. Temporary native tooling and unused Hadamard changes are excluded from this commit.pull/2633/head
273 changed files with 4358 additions and 3329 deletions
@ -1,167 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; |
|||
|
|||
/// <summary>
|
|||
/// Stores the transform-size map consumed by the AV1 deblocking loop filter.
|
|||
/// </summary>
|
|||
internal sealed class Av1LoopFilterContext : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Stores luma transform sizes at plane-relative 4x4 granularity.
|
|||
/// </summary>
|
|||
private readonly MemoryGroup<Av1TransformSize> transformSizesY; |
|||
|
|||
/// <summary>
|
|||
/// The active luma transform-map dimensions in plane-relative 4x4 units.
|
|||
/// </summary>
|
|||
private readonly Size transformSizesYSize; |
|||
|
|||
/// <summary>
|
|||
/// Stores shared-chroma transform sizes at plane-relative 4x4 granularity.
|
|||
/// </summary>
|
|||
private readonly MemoryGroup<Av1TransformSize>? transformSizesUv; |
|||
|
|||
/// <summary>
|
|||
/// The active shared-chroma transform-map dimensions in plane-relative 4x4 units.
|
|||
/// </summary>
|
|||
private readonly Size transformSizesUvSize; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Av1LoopFilterContext"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The allocator that owns the frame-sized transform maps.</param>
|
|||
/// <param name="sequenceHeader">The sequence header defining superblock and chroma geometry.</param>
|
|||
/// <param name="frameHeader">The frame header defining active coded dimensions.</param>
|
|||
public Av1LoopFilterContext( |
|||
MemoryAllocator memoryAllocator, |
|||
ObuSequenceHeader sequenceHeader, |
|||
ObuFrameHeader frameHeader) |
|||
{ |
|||
int modeInfoWidth = frameHeader.ModeInfoColumnCount; |
|||
int modeInfoHeight = frameHeader.ModeInfoRowCount; |
|||
MemoryGroup<Av1TransformSize>? transformSizesY = null; |
|||
MemoryGroup<Av1TransformSize>? transformSizesUv = null; |
|||
this.transformSizesUvSize = default; |
|||
|
|||
try |
|||
{ |
|||
long lumaLength = (long)modeInfoWidth * modeInfoHeight; |
|||
transformSizesY = memoryAllocator.AllocateGroup<Av1TransformSize>( |
|||
lumaLength, |
|||
1, |
|||
AllocationOptions.Clean); |
|||
|
|||
if (!sequenceHeader.ColorConfig.IsMonochrome) |
|||
{ |
|||
int subX = sequenceHeader.ColorConfig.SubSamplingX ? 1 : 0; |
|||
int subY = sequenceHeader.ColorConfig.SubSamplingY ? 1 : 0; |
|||
int chromaWidth = Av1Math.DivideLog2Ceiling(modeInfoWidth, subX); |
|||
int chromaHeight = Av1Math.DivideLog2Ceiling(modeInfoHeight, subY); |
|||
long chromaLength = (long)chromaWidth * chromaHeight; |
|||
transformSizesUv = memoryAllocator.AllocateGroup<Av1TransformSize>( |
|||
chromaLength, |
|||
1, |
|||
AllocationOptions.Clean); |
|||
this.transformSizesUvSize = new Size(chromaWidth, chromaHeight); |
|||
} |
|||
|
|||
this.transformSizesY = transformSizesY; |
|||
this.transformSizesYSize = new Size(modeInfoWidth, modeInfoHeight); |
|||
this.transformSizesUv = transformSizesUv; |
|||
} |
|||
catch |
|||
{ |
|||
transformSizesUv?.Dispose(); |
|||
transformSizesY?.Dispose(); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Stores a transform size across every 4x4 position covered by one transform block.
|
|||
/// </summary>
|
|||
/// <param name="plane">The luma or chroma plane.</param>
|
|||
/// <param name="position">The transform origin in plane-relative 4x4 units.</param>
|
|||
/// <param name="transformSize">The transform size.</param>
|
|||
public void SetTransformSize(Av1Plane plane, Point position, Av1TransformSize transformSize) |
|||
{ |
|||
int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv); |
|||
MemoryGroup<Av1TransformSize> transformSizeMap; |
|||
Size transformSizeMapSize; |
|||
if (planeType == (int)Av1PlaneType.Y) |
|||
{ |
|||
transformSizeMap = this.transformSizesY; |
|||
transformSizeMapSize = this.transformSizesYSize; |
|||
} |
|||
else |
|||
{ |
|||
transformSizeMap = this.transformSizesUv |
|||
?? throw new InvalidOperationException("A monochrome AV1 frame has no chroma transform-size map."); |
|||
|
|||
transformSizeMapSize = this.transformSizesUvSize; |
|||
} |
|||
|
|||
int width = Math.Min(transformSize.Get4x4WideCount(), transformSizeMapSize.Width - position.X); |
|||
int height = Math.Min(transformSize.Get4x4HighCount(), transformSizeMapSize.Height - position.Y); |
|||
|
|||
// libaom clips transform coverage to the active plane mi dimensions at frame edges. Each logical row may cross
|
|||
// allocator segments, so fill only the current segment before continuing at the same logical map offset.
|
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
long offset = ((long)(position.Y + y) * transformSizeMapSize.Width) + position.X; |
|||
int remaining = width; |
|||
while (remaining > 0) |
|||
{ |
|||
Span<Av1TransformSize> destination = transformSizeMap.GetRemainingSliceOfBuffer(offset); |
|||
int count = Math.Min(remaining, destination.Length); |
|||
destination[..count].Fill(transformSize); |
|||
offset += count; |
|||
remaining -= count; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the transform size covering a plane-relative 4x4 position.
|
|||
/// </summary>
|
|||
/// <param name="plane">The luma or chroma plane.</param>
|
|||
/// <param name="position">The position in plane-relative 4x4 units.</param>
|
|||
/// <returns>The transform size covering the position.</returns>
|
|||
public Av1TransformSize GetTransformSize(Av1Plane plane, Point position) |
|||
{ |
|||
int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv); |
|||
MemoryGroup<Av1TransformSize> transformSizeMap; |
|||
int width; |
|||
if (planeType == (int)Av1PlaneType.Y) |
|||
{ |
|||
transformSizeMap = this.transformSizesY; |
|||
width = this.transformSizesYSize.Width; |
|||
} |
|||
else |
|||
{ |
|||
transformSizeMap = this.transformSizesUv |
|||
?? throw new InvalidOperationException("A monochrome AV1 frame has no chroma transform-size map."); |
|||
|
|||
width = this.transformSizesUvSize.Width; |
|||
} |
|||
|
|||
long offset = ((long)position.Y * width) + position.X; |
|||
return transformSizeMap.GetRemainingSliceOfBuffer(offset)[0]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the allocator-owned transform-size maps.
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
this.transformSizesUv?.Dispose(); |
|||
this.transformSizesY.Dispose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha; |
|||
|
|||
/// <summary>
|
|||
/// Supplies normalized auxiliary samples for the exact color region being converted.
|
|||
/// </summary>
|
|||
internal abstract class HeifAlphaRowSource : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Reads the next row in increasing source-row order.
|
|||
/// </summary>
|
|||
/// <param name="y">The row relative to the selected color region.</param>
|
|||
/// <returns>The normalized samples, valid until the next row is read.</returns>
|
|||
public abstract Span<float> ReadRow(int y); |
|||
|
|||
/// <inheritdoc/>
|
|||
public abstract void Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads or resamples native auxiliary samples without packing them into image pixels.
|
|||
/// </summary>
|
|||
/// <typeparam name="TBuffer">The native component-plane view.</typeparam>
|
|||
/// <typeparam name="TSample">The native unsigned sample type.</typeparam>
|
|||
/// <typeparam name="TLoader">The sample widening operator.</typeparam>
|
|||
internal sealed class HeifAlphaRowSource<TBuffer, TSample, TLoader> : HeifAlphaRowSource |
|||
where TBuffer : struct, IHeifPlanarSampleBuffer<TSample> |
|||
where TSample : unmanaged |
|||
where TLoader : struct, IHeifSampleConverter<TSample> |
|||
{ |
|||
private readonly TBuffer buffer; |
|||
private readonly HeifColorConversionParameters parameters; |
|||
private readonly Rectangle window; |
|||
private readonly IMemoryOwner<float> rowOwner; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HeifAlphaRowSource{TBuffer, TSample, TLoader}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration providing scratch storage.</param>
|
|||
/// <param name="buffer">The native auxiliary plane retained by the decoder.</param>
|
|||
/// <param name="parameters">The auxiliary sample range.</param>
|
|||
/// <param name="window">The exact color region within that extent.</param>
|
|||
public HeifAlphaRowSource( |
|||
Configuration configuration, |
|||
TBuffer buffer, |
|||
in HeifColorConversionParameters parameters, |
|||
Rectangle window) |
|||
{ |
|||
this.buffer = buffer; |
|||
this.parameters = parameters; |
|||
this.window = window; |
|||
this.rowOwner = configuration.MemoryAllocator.Allocate<float>(window.Width); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override Span<float> ReadRow(int y) |
|||
{ |
|||
Span<float> row = this.rowOwner.GetSpan()[..this.window.Width]; |
|||
ReadOnlySpan<TSample> source = this.buffer.GetLumaRowSpan(this.window.Y + y).Slice(this.window.X, this.window.Width); |
|||
HeifPlanarAlphaCompositor.NormalizeAlphaRow<TSample, TLoader>(source, row, in this.parameters); |
|||
return row; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Dispose() |
|||
{ |
|||
this.rowOwner.Dispose(); |
|||
} |
|||
} |
|||
@ -1,35 +1,26 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha; |
|||
|
|||
/// <summary>
|
|||
/// Decodes one coded HEIF auxiliary alpha item directly into a packed color frame.
|
|||
/// Decodes an auxiliary item into native samples for joint color and alpha conversion.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
|
|||
internal interface IHeifAlphaItemDecoder<TPixel> |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
internal interface IHeifAlphaItemDecoder |
|||
{ |
|||
/// <summary>
|
|||
/// Decodes and composes one coded auxiliary alpha item.
|
|||
/// Decodes the auxiliary item and transfers its native sample plane to the caller.
|
|||
/// </summary>
|
|||
/// <param name="options">The general options governing the containing HEIF decode.</param>
|
|||
/// <param name="item">The auxiliary image item whose encoded payload is being decoded.</param>
|
|||
/// <param name="options">The options governing payload validation and allocation.</param>
|
|||
/// <param name="item">The auxiliary image item.</param>
|
|||
/// <param name="data">The encoded auxiliary payload.</param>
|
|||
/// <param name="destination">The packed color frame receiving alpha values.</param>
|
|||
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
|
|||
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param>
|
|||
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
|
|||
/// <param name="cancellationToken">The token used to cancel the payload decode.</param>
|
|||
public void DecodeAlphaItemData( |
|||
/// <param name="cancellationToken">The cancellation token.</param>
|
|||
/// <returns>The native plane owned by the caller.</returns>
|
|||
public Av1FrameBuffer<byte> DecodeAlphaItemData( |
|||
DecoderOptions options, |
|||
HeifItem item, |
|||
Span<byte> data, |
|||
ImageFrame<TPixel> destination, |
|||
Size outputSize, |
|||
Rectangle destinationRectangle, |
|||
bool premultiplied, |
|||
CancellationToken cancellationToken); |
|||
} |
|||
|
|||
@ -0,0 +1,100 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.Intrinsics; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Components; |
|||
|
|||
internal abstract partial class HeifColorConverterBase |
|||
{ |
|||
/// <summary>
|
|||
/// Unassociates normalized RGB components before color-profile conversion.
|
|||
/// </summary>
|
|||
/// <param name="red">The associated red components.</param>
|
|||
/// <param name="green">The associated green components.</param>
|
|||
/// <param name="blue">The associated blue components.</param>
|
|||
/// <param name="alpha">The normalized auxiliary samples.</param>
|
|||
public static void UnassociateRgb(Span<float> red, Span<float> green, Span<float> blue, ReadOnlySpan<float> alpha) |
|||
{ |
|||
ref float r = ref MemoryMarshal.GetReference(red); |
|||
ref float g = ref MemoryMarshal.GetReference(green); |
|||
ref float b = ref MemoryMarshal.GetReference(blue); |
|||
ref float a = ref MemoryMarshal.GetReference(alpha); |
|||
int x = 0; |
|||
|
|||
// Each lane is one pixel, with the same lane index in all four planes. Divide the
|
|||
// three color vectors by the alpha vector directly; no interleaving or lane broadcast
|
|||
// is needed. Zero alpha preserves hidden RGB, matching the shared unassociation contract.
|
|||
if (Vector512.IsHardwareAccelerated) |
|||
{ |
|||
for (; x <= red.Length - Vector512<float>.Count; x += Vector512<float>.Count) |
|||
{ |
|||
Vector512<float> av = Vector512.LoadUnsafe(ref a, (nuint)x); |
|||
Vector512<float> rv = Vector512.LoadUnsafe(ref r, (nuint)x); |
|||
Vector512<float> gv = Vector512.LoadUnsafe(ref g, (nuint)x); |
|||
Vector512<float> bv = Vector512.LoadUnsafe(ref b, (nuint)x); |
|||
Vector512<float> zeroAlpha = Vector512.Equals(av, Vector512<float>.Zero); |
|||
rv = Vector512.ConditionalSelect(zeroAlpha, rv, rv / av); |
|||
gv = Vector512.ConditionalSelect(zeroAlpha, gv, gv / av); |
|||
bv = Vector512.ConditionalSelect(zeroAlpha, bv, bv / av); |
|||
Vector512.Clamp(rv, Vector512<float>.Zero, Vector512.Create(1F)).StoreUnsafe(ref r, (nuint)x); |
|||
Vector512.Clamp(gv, Vector512<float>.Zero, Vector512.Create(1F)).StoreUnsafe(ref g, (nuint)x); |
|||
Vector512.Clamp(bv, Vector512<float>.Zero, Vector512.Create(1F)).StoreUnsafe(ref b, (nuint)x); |
|||
} |
|||
} |
|||
|
|||
if (Vector256.IsHardwareAccelerated) |
|||
{ |
|||
for (; x <= red.Length - Vector256<float>.Count; x += Vector256<float>.Count) |
|||
{ |
|||
Vector256<float> av = Vector256.LoadUnsafe(ref a, (nuint)x); |
|||
Vector256<float> rv = Vector256.LoadUnsafe(ref r, (nuint)x); |
|||
Vector256<float> gv = Vector256.LoadUnsafe(ref g, (nuint)x); |
|||
Vector256<float> bv = Vector256.LoadUnsafe(ref b, (nuint)x); |
|||
Vector256<float> zeroAlpha = Vector256.Equals(av, Vector256<float>.Zero); |
|||
rv = Vector256.ConditionalSelect(zeroAlpha, rv, rv / av); |
|||
gv = Vector256.ConditionalSelect(zeroAlpha, gv, gv / av); |
|||
bv = Vector256.ConditionalSelect(zeroAlpha, bv, bv / av); |
|||
Vector256.Clamp(rv, Vector256<float>.Zero, Vector256.Create(1F)).StoreUnsafe(ref r, (nuint)x); |
|||
Vector256.Clamp(gv, Vector256<float>.Zero, Vector256.Create(1F)).StoreUnsafe(ref g, (nuint)x); |
|||
Vector256.Clamp(bv, Vector256<float>.Zero, Vector256.Create(1F)).StoreUnsafe(ref b, (nuint)x); |
|||
} |
|||
} |
|||
|
|||
if (Vector128.IsHardwareAccelerated) |
|||
{ |
|||
for (; x <= red.Length - Vector128<float>.Count; x += Vector128<float>.Count) |
|||
{ |
|||
Vector128<float> av = Vector128.LoadUnsafe(ref a, (nuint)x); |
|||
Vector128<float> rv = Vector128.LoadUnsafe(ref r, (nuint)x); |
|||
Vector128<float> gv = Vector128.LoadUnsafe(ref g, (nuint)x); |
|||
Vector128<float> bv = Vector128.LoadUnsafe(ref b, (nuint)x); |
|||
Vector128<float> zeroAlpha = Vector128.Equals(av, Vector128<float>.Zero); |
|||
rv = Vector128.ConditionalSelect(zeroAlpha, rv, rv / av); |
|||
gv = Vector128.ConditionalSelect(zeroAlpha, gv, gv / av); |
|||
bv = Vector128.ConditionalSelect(zeroAlpha, bv, bv / av); |
|||
Vector128.Clamp(rv, Vector128<float>.Zero, Vector128.Create(1F)).StoreUnsafe(ref r, (nuint)x); |
|||
Vector128.Clamp(gv, Vector128<float>.Zero, Vector128.Create(1F)).StoreUnsafe(ref g, (nuint)x); |
|||
Vector128.Clamp(bv, Vector128<float>.Zero, Vector128.Create(1F)).StoreUnsafe(ref b, (nuint)x); |
|||
} |
|||
} |
|||
|
|||
// Successively narrower vectors consume their own tails. The remaining zero to three
|
|||
// pixels use the same division and clamp without touching the auxiliary plane.
|
|||
for (; x < red.Length; x++) |
|||
{ |
|||
float value = alpha[x]; |
|||
if (value != 0F) |
|||
{ |
|||
red[x] /= value; |
|||
green[x] /= value; |
|||
blue[x] /= value; |
|||
} |
|||
|
|||
red[x] = Math.Clamp(red[x], 0F, 1F); |
|||
green[x] = Math.Clamp(green[x], 0F, 1F); |
|||
blue[x] = Math.Clamp(blue[x], 0F, 1F); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,235 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using System.Numerics.Tensors; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.Intrinsics; |
|||
using SixLabors.ImageSharp.ColorProfiles; |
|||
using SixLabors.ImageSharp.Common.Helpers; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Components; |
|||
|
|||
internal abstract partial class HeifColorConverterBase |
|||
{ |
|||
/// <summary>
|
|||
/// Converts normalized unassociated source components to sRGB before pixel packing.
|
|||
/// </summary>
|
|||
/// <param name="red">The normalized red or monochrome component row.</param>
|
|||
/// <param name="green">The normalized green component row.</param>
|
|||
/// <param name="blue">The normalized blue component row.</param>
|
|||
/// <param name="converter">The profile converter selected for this image region.</param>
|
|||
/// <param name="packed">The reusable interleaved RGB row.</param>
|
|||
public void ConvertRgbToSrgbInPlace( |
|||
Span<float> red, |
|||
Span<float> green, |
|||
Span<float> blue, |
|||
ColorProfileConverter converter, |
|||
Span<Rgb> packed) |
|||
{ |
|||
// A monochrome profile consumes one normalized channel. Color profiles consume the
|
|||
// RGB produced by the signaled H.273 operator, not the encoded YUV components.
|
|||
if (this.IsMonochrome) |
|||
{ |
|||
// Limited-range samples can reconstruct outside the nominal interval. ICC device
|
|||
// curves address that interval, so clip before looking up their transfer functions.
|
|||
TensorPrimitives.Clamp(red, 0F, 1F, red); |
|||
converter.Convert<Y, Rgb>(MemoryMarshal.Cast<float, Y>(red), packed); |
|||
} |
|||
else |
|||
{ |
|||
Interleave3(red, green, blue, MemoryMarshal.Cast<Rgb, float>(packed)); |
|||
converter.Convert<Rgb, Rgb>(packed, packed); |
|||
} |
|||
|
|||
UnpackDeinterleave3(MemoryMarshal.Cast<Rgb, Vector3>(packed), red, green, blue); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Interleaves three planar component lanes into packed XYZ values.
|
|||
/// </summary>
|
|||
/// <param name="xLane">The planar X components.</param>
|
|||
/// <param name="yLane">The planar Y components.</param>
|
|||
/// <param name="zLane">The planar Z components.</param>
|
|||
/// <param name="packed">The destination ordered as consecutive XYZ triples.</param>
|
|||
private static void Interleave3( |
|||
ReadOnlySpan<float> xLane, |
|||
ReadOnlySpan<float> yLane, |
|||
ReadOnlySpan<float> zLane, |
|||
Span<float> packed) |
|||
{ |
|||
DebugGuard.IsTrue(packed.Length % 3 == 0, "Packed length must be divisible by 3."); |
|||
DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); |
|||
DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); |
|||
DebugGuard.MustBeLessThanOrEqualTo(packed.Length / 3, xLane.Length, nameof(packed)); |
|||
|
|||
ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); |
|||
ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); |
|||
ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); |
|||
ref float packedRef = ref MemoryMarshal.GetReference(packed); |
|||
int i = 0; |
|||
|
|||
if (Vector128.IsHardwareAccelerated) |
|||
{ |
|||
int oneVectorFromEnd = xLane.Length - Vector128<float>.Count; |
|||
|
|||
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count) |
|||
{ |
|||
// Each source vector contains four consecutive samples from one plane:
|
|||
// x = [X0 X1 X2 X3]
|
|||
// y = [Y0 Y1 Y2 Y3]
|
|||
// z = [Z0 Z1 Z2 Z3]
|
|||
// Shifting X by one sample supplies the value that follows each XYZ triple:
|
|||
// shiftedX = [X1 X2 X3 0]
|
|||
// The transpose therefore produces overlapping rows [Xn Yn Zn Xn+1].
|
|||
// AlignRight joins those rows into three complete destination vectors, avoiding
|
|||
// the scalar-sized stores that writing four independent Vector3 values requires.
|
|||
Vector128<float> x = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref xLaneRef, i)); |
|||
Vector128<float> y = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref yLaneRef, i)); |
|||
Vector128<float> z = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref zLaneRef, i)); |
|||
|
|||
// YUV reconstruction can overshoot the device RGB range. Clip while loading
|
|||
// the planes, before ICC curve lookup, without quantizing through packed pixels.
|
|||
x = Vector128.Clamp(x, Vector128<float>.Zero, Vector128.Create(1F)); |
|||
y = Vector128.Clamp(y, Vector128<float>.Zero, Vector128.Create(1F)); |
|||
z = Vector128.Clamp(z, Vector128<float>.Zero, Vector128.Create(1F)); |
|||
Vector128<float> shiftedX = Vector128_.ShiftRightBytesInVector(x.AsByte(), sizeof(float)).AsSingle(); |
|||
|
|||
Transpose4( |
|||
x, |
|||
y, |
|||
z, |
|||
shiftedX, |
|||
out Vector128<float> pixel0, |
|||
out Vector128<float> pixel1, |
|||
out Vector128<float> pixel2, |
|||
out Vector128<float> pixel3); |
|||
|
|||
// Dropping pixel2.X lets [Y2] complete [Y1 Z1 X2] from pixel1.
|
|||
Vector128<byte> shiftedPixel2 = Vector128_.ShiftRightBytesInVector(pixel2.AsByte(), sizeof(float)); |
|||
Vector128<float> packed1 = Vector128_.AlignRight(shiftedPixel2, pixel1.AsByte(), sizeof(float)).AsSingle(); |
|||
|
|||
// Dropping pixel3.X leaves [Y3 Z3] to complete [Z2 X3] from pixel2.
|
|||
Vector128<byte> shiftedPixel3 = Vector128_.ShiftRightBytesInVector(pixel3.AsByte(), sizeof(float)); |
|||
Vector128<float> packed2 = Vector128_.AlignRight(shiftedPixel3, pixel2.AsByte(), sizeof(float) * 2).AsSingle(); |
|||
|
|||
ref Vector128<float> destination = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref packedRef, (uint)i * 3)); |
|||
|
|||
destination = pixel0; |
|||
Unsafe.Add(ref destination, 1) = packed1; |
|||
Unsafe.Add(ref destination, 2) = packed2; |
|||
} |
|||
} |
|||
|
|||
// Fewer than four pixels remain after SIMD, or every pixel reaches this path
|
|||
// when the runtime cannot accelerate the cross-vector transpose.
|
|||
for (; i < xLane.Length; i++) |
|||
{ |
|||
nuint sourceOffset = (uint)i; |
|||
nuint packedOffset = sourceOffset * 3; |
|||
Unsafe.Add(ref packedRef, packedOffset) = Math.Clamp(Unsafe.Add(ref xLaneRef, sourceOffset), 0F, 1F); |
|||
Unsafe.Add(ref packedRef, packedOffset + 1) = Math.Clamp(Unsafe.Add(ref yLaneRef, sourceOffset), 0F, 1F); |
|||
Unsafe.Add(ref packedRef, packedOffset + 2) = Math.Clamp(Unsafe.Add(ref zLaneRef, sourceOffset), 0F, 1F); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Deinterleaves packed XYZ values into three planar component lanes.
|
|||
/// </summary>
|
|||
/// <param name="packed">The source ordered as consecutive XYZ triples.</param>
|
|||
/// <param name="xLane">The destination X components.</param>
|
|||
/// <param name="yLane">The destination Y components.</param>
|
|||
/// <param name="zLane">The destination Z components.</param>
|
|||
private static void UnpackDeinterleave3(ReadOnlySpan<Vector3> packed, Span<float> xLane, Span<float> yLane, Span<float> zLane) |
|||
{ |
|||
DebugGuard.IsTrue(packed.Length == xLane.Length, nameof(packed), "Channels must be of same size!"); |
|||
DebugGuard.IsTrue(yLane.Length == xLane.Length, nameof(yLane), "Channels must be of same size!"); |
|||
DebugGuard.IsTrue(zLane.Length == xLane.Length, nameof(zLane), "Channels must be of same size!"); |
|||
|
|||
ref float packedRef = ref MemoryMarshal.GetReference(MemoryMarshal.Cast<Vector3, float>(packed)); |
|||
ref float xLaneRef = ref MemoryMarshal.GetReference(xLane); |
|||
ref float yLaneRef = ref MemoryMarshal.GetReference(yLane); |
|||
ref float zLaneRef = ref MemoryMarshal.GetReference(zLane); |
|||
int i = 0; |
|||
|
|||
if (Vector128.IsHardwareAccelerated) |
|||
{ |
|||
int oneVectorFromEnd = packed.Length - Vector128<float>.Count; |
|||
|
|||
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count) |
|||
{ |
|||
// A Vector3 occupies twelve contiguous bytes, so a sixteen-byte load beginning
|
|||
// at one pixel also reads the X component of the following pixel:
|
|||
// pixel0 = [X0 Y0 Z0 X1]
|
|||
// pixel1 = [X1 Y1 Z1 X2]
|
|||
// The transpose discards this fourth column, making the overlap useful padding
|
|||
// and avoiding two insert instructions per pixel. The final row needs explicit
|
|||
// zero padding only when pixel3 is the last element in the source span.
|
|||
nuint packedOffset = (uint)i * 3; |
|||
Vector128<float> pixel0 = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref packedRef, packedOffset)); |
|||
Vector128<float> pixel1 = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref packedRef, packedOffset + 3)); |
|||
Vector128<float> pixel2 = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref packedRef, packedOffset + 6)); |
|||
ref float pixel3Ref = ref Unsafe.Add(ref packedRef, packedOffset + 9); |
|||
Vector128<float> pixel3 = i + Vector128<float>.Count < packed.Length |
|||
? Unsafe.As<float, Vector128<float>>(ref pixel3Ref) |
|||
: Unsafe.As<float, Vector3>(ref pixel3Ref).AsVector128(); |
|||
|
|||
Transpose4(pixel0, pixel1, pixel2, pixel3, out Vector128<float> x, out Vector128<float> y, out Vector128<float> z, out _); |
|||
|
|||
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref xLaneRef, i)) = x; |
|||
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref yLaneRef, i)) = y; |
|||
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref zLaneRef, i)) = z; |
|||
} |
|||
} |
|||
|
|||
// The scalar remainder preserves the original scatter behavior for zero to
|
|||
// three pixels and provides the complete fallback on unsupported hardware.
|
|||
for (; i < packed.Length; i++) |
|||
{ |
|||
nuint packedOffset = (uint)i * 3; |
|||
Unsafe.Add(ref xLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset); |
|||
Unsafe.Add(ref yLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset + 1); |
|||
Unsafe.Add(ref zLaneRef, i) = Unsafe.Add(ref packedRef, packedOffset + 2); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Transposes four four-lane rows into four four-lane columns.
|
|||
/// </summary>
|
|||
/// <param name="row0">The first matrix row.</param>
|
|||
/// <param name="row1">The second matrix row.</param>
|
|||
/// <param name="row2">The third matrix row.</param>
|
|||
/// <param name="row3">The fourth matrix row.</param>
|
|||
/// <param name="column0">The first matrix column.</param>
|
|||
/// <param name="column1">The second matrix column.</param>
|
|||
/// <param name="column2">The third matrix column.</param>
|
|||
/// <param name="column3">The fourth matrix column.</param>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static void Transpose4( |
|||
Vector128<float> row0, |
|||
Vector128<float> row1, |
|||
Vector128<float> row2, |
|||
Vector128<float> row3, |
|||
out Vector128<float> column0, |
|||
out Vector128<float> column1, |
|||
out Vector128<float> column2, |
|||
out Vector128<float> column3) |
|||
{ |
|||
// The first unpack interleaves adjacent 32-bit lanes from rows 0/1 and 2/3:
|
|||
// row01Low = [r0c0 r1c0 r0c1 r1c1]
|
|||
// row23Low = [r2c0 r3c0 r2c1 r3c1]
|
|||
// A second unpack treats each adjacent pair as one 64-bit lane and combines
|
|||
// the row01 and row23 pairs into complete columns. The integer views only
|
|||
// expose the cross-platform unpack helpers; every floating-point bit is preserved.
|
|||
Vector128<int> row01Low = Vector128_.UnpackLow(row0.AsInt32(), row1.AsInt32()); |
|||
Vector128<int> row01High = Vector128_.UnpackHigh(row0.AsInt32(), row1.AsInt32()); |
|||
Vector128<int> row23Low = Vector128_.UnpackLow(row2.AsInt32(), row3.AsInt32()); |
|||
Vector128<int> row23High = Vector128_.UnpackHigh(row2.AsInt32(), row3.AsInt32()); |
|||
|
|||
column0 = Vector128_.UnpackLow(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); |
|||
column1 = Vector128_.UnpackHigh(row01Low.AsInt64(), row23Low.AsInt64()).AsSingle(); |
|||
column2 = Vector128_.UnpackLow(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); |
|||
column3 = Vector128_.UnpackHigh(row01High.AsInt64(), row23High.AsInt64()).AsSingle(); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Specifies how subsampled chroma is expanded when decoding HEIF images.
|
|||
/// </summary>
|
|||
public enum HeifChromaUpsampling |
|||
{ |
|||
/// <summary>
|
|||
/// Uses nearest-neighbour sampling for 8-bit source samples and bilinear interpolation for higher bit depths.
|
|||
/// </summary>
|
|||
Auto, |
|||
|
|||
/// <summary>
|
|||
/// Repeats the nearest chroma sample.
|
|||
/// </summary>
|
|||
NearestNeighbor, |
|||
|
|||
/// <summary>
|
|||
/// Interpolates between neighboring chroma samples.
|
|||
/// </summary>
|
|||
Bilinear |
|||
} |
|||
@ -1,20 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Identifies the compression method used by a coded image item in a HEIF file.
|
|||
/// </summary>
|
|||
public enum HeifCompressionMethod |
|||
{ |
|||
/// <summary>
|
|||
/// Legacy JPEG coding.
|
|||
/// </summary>
|
|||
LegacyJpeg, |
|||
|
|||
/// <summary>
|
|||
/// AOMedia Video 1 (AV1) coding.
|
|||
/// </summary>
|
|||
Av1, |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,18 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Configuration options for decoding HEIF images.
|
|||
/// </summary>
|
|||
public sealed class HeifDecoderOptions : ISpecializedDecoderOptions |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public DecoderOptions GeneralOptions { get; init; } = new(); |
|||
|
|||
/// <summary>
|
|||
/// Gets the chroma upsampling mode. The default is <see cref="HeifChromaUpsampling.Auto"/>.
|
|||
/// </summary>
|
|||
public HeifChromaUpsampling ChromaUpsampling { get; init; } |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Provides shared presentation operations for decoded HEIF image items.
|
|||
/// </summary>
|
|||
internal static class HeifItemDecoderUtilities |
|||
{ |
|||
/// <summary>
|
|||
/// Scales a decoded image to the spatial extent associated with its image item.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The decoded pixel format.</typeparam>
|
|||
/// <param name="image">The decoded image.</param>
|
|||
/// <param name="item">The image item that defines the presented spatial extent.</param>
|
|||
public static void ScaleToItemExtent<TPixel>(Image<TPixel> image, HeifItem item) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
Size extent = item.Extent; |
|||
if (extent == default || (image.Width == extent.Width && image.Height == extent.Height)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// libavif applies box filtering when coded dimensions differ from an item's ispe dimensions. Reusing the
|
|||
// same ImageSharp resampler keeps direct images and grid tiles on one presentation path.
|
|||
image.Mutate(context => context.Resize(extent.Width, extent.Height, KnownResamplers.Box)); |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Maps cropped source pixels to their rotated and mirrored destination coordinates.
|
|||
/// </summary>
|
|||
internal readonly struct HeifPixelTransform |
|||
{ |
|||
private readonly Matrix3x2 orientation; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HeifPixelTransform"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="rotation">The counter-clockwise quarter-turn count.</param>
|
|||
/// <param name="mirrorAxis">The optional destination mirror axis.</param>
|
|||
public HeifPixelTransform(byte rotation, byte? mirrorAxis) |
|||
{ |
|||
this.orientation = Matrix3x2.CreateRotation(-rotation * MathF.PI / 2); |
|||
this.orientation *= Matrix3x2.CreateScale(mirrorAxis == 1 ? -1 : 1, mirrorAxis == 0 ? -1 : 1); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether source rows map directly to destination rows.
|
|||
/// </summary>
|
|||
public bool IsIdentity => this.orientation == default || this.orientation.IsIdentity; |
|||
|
|||
/// <summary>
|
|||
/// Gets the destination extent for a cropped source extent.
|
|||
/// </summary>
|
|||
/// <param name="sourceSize">The cropped source extent.</param>
|
|||
/// <returns>The destination extent.</returns>
|
|||
public Size GetDestinationSize(Size sourceSize) |
|||
=> Rectangle.Round(Rectangle.Transform(new Rectangle(Point.Empty, sourceSize), this.GetMatrix(sourceSize))).Size; |
|||
|
|||
/// <summary>
|
|||
/// Finds the source rectangle corresponding to a destination window.
|
|||
/// </summary>
|
|||
/// <param name="destination">The window in transformed coordinates.</param>
|
|||
/// <param name="sourceSize">The complete cropped source extent.</param>
|
|||
/// <returns>The source window before rotation and mirroring.</returns>
|
|||
public Rectangle GetSourceRectangle(Rectangle destination, Size sourceSize) |
|||
{ |
|||
Matrix3x2.Invert(this.GetMatrix(sourceSize), out Matrix3x2 inverse); |
|||
return Rectangle.Round(Rectangle.Transform(destination, inverse)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Maps one source pixel to the destination.
|
|||
/// </summary>
|
|||
/// <param name="x">The source column relative to the crop.</param>
|
|||
/// <param name="y">The source row relative to the crop.</param>
|
|||
/// <param name="matrix">The matrix resolved once for the source region.</param>
|
|||
/// <returns>The destination pixel coordinate.</returns>
|
|||
public static Point Transform(int x, int y, Matrix3x2 matrix) |
|||
{ |
|||
Vector2 point = Vector2.Transform(new Vector2(x + 0.5F, y + 0.5F), matrix); |
|||
return new Point((int)MathF.Floor(point.X), (int)MathF.Floor(point.Y)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Maps a source rectangle to its exact destination rectangle.
|
|||
/// </summary>
|
|||
/// <param name="source">The rectangle relative to the cropped source.</param>
|
|||
/// <param name="sourceSize">The complete cropped source extent.</param>
|
|||
/// <returns>The destination rectangle.</returns>
|
|||
public Rectangle TransformRectangle(Rectangle source, Size sourceSize) |
|||
{ |
|||
return Rectangle.Round(Rectangle.Transform(source, this.GetMatrix(sourceSize))); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes a converted source row into its destination coordinates.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The packed pixel type.</typeparam>
|
|||
/// <param name="row">The converted source row.</param>
|
|||
/// <param name="y">The source row index relative to the crop.</param>
|
|||
/// <param name="matrix">The matrix resolved once for the source region.</param>
|
|||
/// <param name="destination">The exact destination region.</param>
|
|||
public static void WriteRow<TPixel>(ReadOnlySpan<TPixel> row, int y, Matrix3x2 matrix, Buffer2DRegion<TPixel> destination) |
|||
where TPixel : unmanaged |
|||
{ |
|||
// The matrix's first basis vector is the integer destination step for one source column.
|
|||
// Quarter turns and mirrors require no matrix operations inside the pixel loop.
|
|||
Point first = Transform(0, y, matrix); |
|||
int stepX = (int)matrix.M11; |
|||
int stepY = (int)matrix.M12; |
|||
|
|||
for (int x = 0; x < row.Length; x++) |
|||
{ |
|||
destination.DangerousGetRowSpan(first.Y)[first.X] = row[x]; |
|||
first.X += stepX; |
|||
first.Y += stepY; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the transform translated into the positive destination bounds.
|
|||
/// </summary>
|
|||
/// <param name="sourceSize">The source extent.</param>
|
|||
/// <returns>The transform of rectangle edges and pixel centers.</returns>
|
|||
public Matrix3x2 GetMatrix(Size sourceSize) |
|||
{ |
|||
Matrix3x2 matrix = this.IsIdentity ? Matrix3x2.Identity : this.orientation; |
|||
RectangleF bounds = Rectangle.Transform(new Rectangle(Point.Empty, sourceSize), matrix); |
|||
matrix.Translation = new Vector2(-bounds.X, -bounds.Y); |
|||
return matrix; |
|||
} |
|||
} |
|||
@ -1,52 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Jpeg; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Decodes a single JPEG-coded HEIF image item.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
|
|||
internal class JpegHeifItemDecoder<TPixel> : IHeifItemDecoder<TPixel> |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the JPEG-coded image item type.
|
|||
/// </summary>
|
|||
public Heif4CharCode Type => Heif4CharCode.Jpeg; |
|||
|
|||
/// <summary>
|
|||
/// Gets the legacy JPEG compression method.
|
|||
/// </summary>
|
|||
public HeifCompressionMethod CompressionMethod => HeifCompressionMethod.LegacyJpeg; |
|||
|
|||
/// <summary>
|
|||
/// Decodes the encoded JPEG payload of an image item.
|
|||
/// </summary>
|
|||
/// <param name="options">The general options governing the containing HEIF decode.</param>
|
|||
/// <param name="item">The HEIF item whose encoded payload is being decoded.</param>
|
|||
/// <param name="data">The encoded JPEG payload.</param>
|
|||
/// <param name="colorProfile">The container color description associated with the image item.</param>
|
|||
/// <param name="cancellationToken">The token used to cancel the payload decode.</param>
|
|||
/// <returns>The decoded image.</returns>
|
|||
public unsafe Image<TPixel> DecodeItemData( |
|||
DecoderOptions options, |
|||
HeifItem item, |
|||
Span<byte> data, |
|||
CicpProfile? colorProfile, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
// The JPEG decoder owns the payload's JPEG color coding. The containing decoder attaches HEIF CICP as
|
|||
// presentation metadata after payload decode, so it must not be mistaken for JPEG component-transform syntax.
|
|||
fixed (byte* dataPointer = data) |
|||
{ |
|||
using UnmanagedMemoryStream stream = new(dataPointer, data.Length); |
|||
using JpegDecoderCore decoder = new(new JpegDecoderOptions { GeneralOptions = options }); |
|||
return decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken); |
|||
} |
|||
} |
|||
} |
|||
@ -1,208 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.Formats.Heif; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; |
|||
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; |
|||
using SixLabors.ImageSharp.Formats.Heif.Components; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Tests; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Measures encoding a photographic sequence with fractional motion at the interpolation-search effort boundaries.
|
|||
/// </summary>
|
|||
[MemoryDiagnoser] |
|||
public class Av1SequenceEncoderBenchmarks |
|||
{ |
|||
/// <summary>
|
|||
/// The number of displayed pictures in each independently encoded sequence.
|
|||
/// </summary>
|
|||
private const int FrameCount = 3; |
|||
|
|||
/// <summary>
|
|||
/// The native AV1 quantizer index corresponding to libaom's public constant-quality level 30.
|
|||
/// </summary>
|
|||
private const int QIndex = 120; |
|||
|
|||
/// <summary>
|
|||
/// The fixed native speed baseline, independent of ImageSharp's effort scale.
|
|||
/// </summary>
|
|||
private const int NativeCpuUsed = 6; |
|||
|
|||
/// <summary>
|
|||
/// The native public quantizer corresponding to <see cref="QIndex"/>, also used for both rate-control bounds.
|
|||
/// </summary>
|
|||
private const int NativeQuality = 30; |
|||
|
|||
private Image<Rgb24> sequence; |
|||
private Configuration configuration; |
|||
private ObuColorConfig colorConfig; |
|||
private MemoryStream output; |
|||
private string outputDirectory; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the square frame dimension.
|
|||
/// </summary>
|
|||
[Params(256, 512)] |
|||
public int Dimension { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the effort controlling fixed, common switchable, or independently switchable filters.
|
|||
/// </summary>
|
|||
[Params(7, 8, 9)] |
|||
public int Effort { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Prepares identical photographic RGB frames and a planar source file for checking reconstructed output quality.
|
|||
/// </summary>
|
|||
[GlobalSetup] |
|||
public void Setup() |
|||
{ |
|||
this.configuration = Configuration.Default.Clone(); |
|||
this.configuration.MaxDegreeOfParallelism = 1; |
|||
this.colorConfig = new ObuColorConfig |
|||
{ |
|||
BitDepth = Av1BitDepth.EightBit, |
|||
IsColorDescriptionPresent = true, |
|||
ColorPrimaries = ObuColorPrimaries.Bt601, |
|||
TransferCharacteristics = ObuTransferCharacteristics.Bt601, |
|||
MatrixCoefficients = ObuMatrixCoefficients.Bt601, |
|||
ColorRange = true, |
|||
SubSamplingX = true, |
|||
SubSamplingY = true, |
|||
ChromaSamplePosition = ObuChromoSamplePosition.Unknown |
|||
}; |
|||
|
|||
this.output = new MemoryStream(); |
|||
this.outputDirectory = TestEnvironment.CreateOutputDirectory("Heif", "Av1", nameof(Av1SequenceEncoderBenchmarks)); |
|||
string inputPath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Png.Bike); |
|||
using Image<Rgb24> photograph = Image.Load<Rgb24>(inputPath); |
|||
|
|||
// Leave a source margin for the half-pixel translations. Resampling is setup work, not encoder time;
|
|||
// every invocation consumes the same three images rather than repeatedly translating a previous result.
|
|||
photograph.Mutate(context => context.Resize(new ResizeOptions |
|||
{ |
|||
Size = new Size(this.Dimension + FrameCount, this.Dimension + FrameCount), |
|||
Mode = ResizeMode.Crop |
|||
})); |
|||
|
|||
Rectangle sourceBounds = new(0, 0, photograph.Width, photograph.Height); |
|||
Size targetSize = new(this.Dimension, this.Dimension); |
|||
this.sequence = photograph.Clone(context => context.Crop(new Rectangle(Point.Empty, targetSize))); |
|||
for (int frameIndex = 1; frameIndex < FrameCount; frameIndex++) |
|||
{ |
|||
Matrix3x2 translation = Matrix3x2.CreateTranslation(-0.5F * frameIndex, -0.5F * frameIndex); |
|||
using Image<Rgb24> translated = photograph.Clone(context => |
|||
context.Transform(sourceBounds, translation, targetSize, KnownResamplers.Bicubic)); |
|||
|
|||
this.sequence.Frames.AddFrame(translated.Frames.RootFrame); |
|||
} |
|||
|
|||
// Export the production-converted source planes only for checking reconstructed output quality.
|
|||
// Neither timed encoder reads this file: both convert the original RGB frames during each operation.
|
|||
using Av1EncoderFrameBuffer<byte> planar = new(this.configuration, this.Dimension, this.Dimension, 8, Av1ColorFormat.Yuv420, 0, 0, lumaBorder: 64); |
|||
using FileStream raw = File.Create(Path.Combine(this.outputDirectory, $"bike-{this.Dimension}-3frames.source.yuv")); |
|||
foreach (ImageFrame<Rgb24> frame in this.sequence.Frames) |
|||
{ |
|||
Av1FrameEncoder.PrepareSource(this.configuration, frame, planar.Frame, this.colorConfig); |
|||
for (int planeIndex = 0; planeIndex < this.colorConfig.PlaneCount; planeIndex++) |
|||
{ |
|||
Buffer2DRegion<byte> plane = planar.Frame.View.GetPlane((Av1Plane)planeIndex); |
|||
for (int y = 0; y < plane.Height; y++) |
|||
{ |
|||
raw.Write(plane.DangerousGetRowSpan(y)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Encodes one key picture and two dependent pictures, returning the complete OBU payload length.
|
|||
/// </summary>
|
|||
/// <returns>The encoded sequence length.</returns>
|
|||
[Benchmark] |
|||
public long ImageSharp() |
|||
{ |
|||
this.output.SetLength(0); |
|||
using Av1FrameEncoder.SequenceEncoder encoder = Av1FrameEncoder.CreateColorSequenceEncoder( |
|||
this.configuration, |
|||
this.Dimension, |
|||
this.Dimension, |
|||
this.colorConfig, |
|||
QIndex, |
|||
this.Effort, |
|||
speed: HeifEncodingSpeed.Level0); |
|||
|
|||
// One operation owns the real sequence lifetime: allocation, conversion, key/inter coding, and disposal.
|
|||
// The caller's destination is reused, excluding filesystem and MemoryStream growth from steady-state timing.
|
|||
encoder.EncodeKeyFrame(this.sequence.Frames.RootFrame, this.output); |
|||
for (int frameIndex = 1; frameIndex < FrameCount; frameIndex++) |
|||
{ |
|||
encoder.EncodeInterFrame(this.sequence.Frames[frameIndex], this.output); |
|||
} |
|||
|
|||
return this.output.Length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Encodes the same RGB sequence with current-main libaom, including conversion, allocation, output, and disposal.
|
|||
/// </summary>
|
|||
/// <returns>The encoded sequence length.</returns>
|
|||
[Benchmark(Baseline = true)] |
|||
public long Libaom() |
|||
{ |
|||
// cpu-used is a separate speed scale, not an ImageSharp effort mapping. Keep the reference at
|
|||
// good-quality speed six while comparing the three managed interpolation-search boundaries.
|
|||
this.output.SetLength(0); |
|||
using LibaomBenchmarkEncoder encoder = LibaomBenchmarkEncoder.Open(this.Dimension, this.Dimension, NativeQuality, NativeCpuUsed); |
|||
using Av1EncoderFrameBuffer<byte> planar = new(this.configuration, this.Dimension, this.Dimension, 8, Av1ColorFormat.Yuv420, 0, 0, lumaBorder: 64); |
|||
using Av1FrameEncoder.Av1EncoderConversionWorkspace conversion = new(this.configuration, this.Dimension, this.colorConfig, false, false); |
|||
Rectangle bounds = new(0, 0, this.Dimension, this.Dimension); |
|||
for (int frameIndex = 0; frameIndex < FrameCount; frameIndex++) |
|||
{ |
|||
// Conversion belongs inside both measured paths. Reuse the same row workspace and SIMD converter
|
|||
// as the managed sequence encoder, writing directly into the planes passed to native libaom.
|
|||
conversion.Convert<Rgb24, Av1EncoderFrame<byte>.PlanarView, byte, HeifByteSampleConverter>( |
|||
this.configuration, this.sequence.Frames[frameIndex], bounds, planar.Frame.View); |
|||
|
|||
encoder.Encode(planar.Frame, frameIndex, this.output); |
|||
} |
|||
|
|||
encoder.Finish(this.output); |
|||
return this.output.Length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retains the measured managed encoder output and releases the input images and destination stream.
|
|||
/// </summary>
|
|||
[GlobalCleanup(Target = nameof(ImageSharp))] |
|||
public void CleanupImageSharp() => this.Cleanup($"bike-{this.Dimension}-q{QIndex}-effort{this.Effort}.obu"); |
|||
|
|||
/// <summary>
|
|||
/// Retains the measured reference encoder output and releases the input images and destination stream.
|
|||
/// </summary>
|
|||
[GlobalCleanup(Target = nameof(Libaom))] |
|||
public void CleanupLibaom() => this.Cleanup($"bike-{this.Dimension}-q{QIndex}-libaom-cpu{NativeCpuUsed}.obu"); |
|||
|
|||
/// <summary>
|
|||
/// Writes the measured payload without another encoding pass and releases the shared benchmark resources.
|
|||
/// </summary>
|
|||
/// <param name="outputName">The codec-specific payload file name.</param>
|
|||
private void Cleanup(string outputName) |
|||
{ |
|||
// Output validation and quality measurement use the actual measured payload, with no encode or decode
|
|||
// hidden inside the timed operation and no file-sized ToArray copy.
|
|||
using FileStream encoded = File.Create(Path.Combine(this.outputDirectory, outputName)); |
|||
this.output.Position = 0; |
|||
this.output.CopyTo(encoded); |
|||
this.output.Dispose(); |
|||
this.sequence.Dispose(); |
|||
} |
|||
} |
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:19f3e0d5357df5dacf16dc73501e5bbe280f04e0f0e0831bfc8f2511497dccf4 |
|||
size 408944 |
|||
oid sha256:568c8dfeb43b36e9f2df3c7edd8a16bcd8fde52a259e61c04d903b0e0767cc62 |
|||
size 371942 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:2098b78efb22b27473802e4e9e074d4948eda6d6bb06510069c9497e63f37bf5 |
|||
size 383051 |
|||
oid sha256:3c5ee8ff354f1c48d5746af02d76708e7be76d3947f318f9db5855909a804f9e |
|||
size 346662 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:cd995bb10a9c10eba89584ea49c6a1c853b0168aa3fac324d3d935d9f985851f |
|||
size 383407 |
|||
oid sha256:c5ed33348681cf5fd780ff38afd21f49651fd8a6256b91c8f31d88a315294100 |
|||
size 345399 |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:8524059e28b1f64e6070108794213bfe0e87b654d37a7c405e6918d72fc312f9 |
|||
size 204802 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:3f2cf98bfaacc2ff6a229d433cdc0c7ba068447d25d5cd5ca7fc04ae4f9ea07c |
|||
size 352 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:528eb9a1e6fe6e5060265a2d02bb2f314f11fd4274efd9971f0a66aeaa403087 |
|||
size 695 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:e9973399a7c4d670532905dfcda7878b9709f438136233974580e822e355688d |
|||
size 697 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:d30d25fe8302f64c9764b5cc38f93b68922d45f2fa5c5068d4c2308e5bec16fc |
|||
size 716 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a780bfbfd53e0c14477fe3fffdc7d4e3e571988fc49872c04620555328d86f91 |
|||
size 697 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:bbc64e3c45c4f02c334a528be2e87a99ac5f854382ae36d7ce2bce0fe9c492e9 |
|||
size 697 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:fbf8f4f98d052a9ebc1eeaec5253b545004a8094bd9d70662d269efbd946e333 |
|||
size 695 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:0c4f51868abaac1ad9742c6be51b897319a47d3e8f5be06163896ed3ef5ccc56 |
|||
size 697 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:6443ae807d43ffa6df3e350358081e3ace6fe79fdf2fabf4dae0e79fc7ac90de |
|||
size 696 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:6a288fa21457152d6e9b543a679f1cd0b62e08b157c06bc70b71ad33271ec297 |
|||
size 697 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:32de0769629d5043bfe3219f596eac3c30cbfefa371e0126edb0a4858981f95d |
|||
size 697 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:5d911da2c28fec56a4c11badf7a04b03846b86ba214ce6aaeebe43d696bb9668 |
|||
size 695 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:e0957ec2347edf15744a3914a471ee0412402b8e034e0311d906234e54e4c441 |
|||
size 697 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:b6c8c30a7551f7fbcfd290f55dbcdb04f93ec0a72cb194c9c0ae9bfcbc2fd6d2 |
|||
size 696 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:ecdf8e090b33738a0d7c5aa484f291f6e9ef8ea8b3467bd63de1a103ca3e40a2 |
|||
size 697 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:5d939fd84c88f09609286cf606257534fd571e5acf36194589c22a7d904e94c6 |
|||
size 697 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:176d81c3d9b8fb8760d69f11d0804673e4a55704e4110b5eae163a64c9aee62a |
|||
size 34758 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:4268ff56bddccd15637c35d7108d59dd8d2c30a54024d123e1f41250a571dbd0 |
|||
size 43380 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:568c8dfeb43b36e9f2df3c7edd8a16bcd8fde52a259e61c04d903b0e0767cc62 |
|||
size 371942 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:4cd476ce81e54eed777e4218f9b0ad38cc509d60d1c38bb0ba2ccc013d4ee053 |
|||
size 6462 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:97aa2d6df27bb8ba5647d08bd0e3da8f25c2411a7f4cabb2ba490012e218aa2f |
|||
size 355642 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a9b2d70a65299c6da81c3b495c54e1eab99c1932e44add57cff2ae00c1f36072 |
|||
size 834 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1148ebf6aa4b0f2d069d5e9b9605f6fb2a315e525f18016cdcae23efdd81da84 |
|||
size 157 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:260a860d1f01e56c0154a31a6f4802fd27b5f507f724d55084f5278b388003c2 |
|||
size 5294 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:2b39c31c012ad7e194870fcdd074a83f225dc0dfc40731f0c72fa8847821e0fd |
|||
size 9631 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:253291bc0bcebde6cc0ef6729c52ec025cba4a7ed046a3231297da7c63df3f4a |
|||
size 9748 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:96423c89e68f6d4e6a349e0693b1c0b106055654681d51c822ab43fd12b86223 |
|||
size 9703 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:0758c17dc36e38aee9f4389a335c2bf332ab91e4c79d7b0b22994fddd0fd1605 |
|||
size 186 |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue