Browse Source

Implement JPEG-style AV1 color conversion

pull/2633/head
James Jackson-South 1 week ago
parent
commit
1f8bef166c
  1. 17
      HEIF_IMPLEMENTATION_PLAN.md
  2. 155
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConversionParameters.cs
  3. 156
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.CoefficientOperator.cs
  4. 249
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.ConstantLuminanceOperator.cs
  5. 526
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.ICtCpOperator.cs
  6. 138
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.IdentityOperator.cs
  7. 427
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.Operator.cs
  8. 143
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.Smpte2085Operator.cs
  9. 141
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.YCgCoOperator.cs
  10. 126
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.cs
  11. 40
      src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs
  12. 623
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Operators.cs
  13. 426
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs
  14. 156
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowOperation.cs
  15. 409
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs
  16. 824
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs
  17. 129
      tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs
  18. 319
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs

17
HEIF_IMPLEMENTATION_PLAN.md

@ -34,12 +34,19 @@ Checkboxes may be marked complete only when the implementation and the verificat
- [x] Preserve the exact ICC payload from an independently encoded AVIF primary item.
- [x] Prove that a genuine non-sRGB AVIF profile changes decoded pixels and matches the independently converted source image within the documented AV1 tolerance.
- [x] Verify canonical-sRGB compaction and metadata skipping independently from ICC preservation and color conversion.
- [x] Replace the AV1 RGB/YUV arithmetic layer with JPEG's closed-generic static operator pattern in both directions: frame-scoped operator selection, in-place planar decode, RGB inputs with `out` component planes for encode, `Vector512`/`Vector256`/`Vector128` traversal, and one scalar tail.
- [x] Keep codec row scheduling sequential and reuse frame-scoped allocator-backed buffers. Reuse JPEG's optimized `UnpackIntoRgbPlanes` contract for 8-bit encoding and pooled `Rgb48` staging for 10/12-bit encoding so high-precision input is not truncated through an eight-bit or `Vector4` intermediate.
- [x] Implement SIMD range expansion, chroma reconstruction, matrix/transfer conversion, clamping, and RGB packing for coefficient YCbCr, identity GBR, YCgCo, constant luminance, SMPTE ST 2085, and PQ/HLG ICtCp, with focused SIMD-lane/scalar-tail parity coverage.
- [x] Implement SIMD 4:2:0 and 4:2:2 encoder downsampling with odd-width and odd-height tails, and verify the stored 8/12-bit chroma samples against independently encoded full-resolution planes.
- [ ] Complete the SIMD YUV/CICP paths for every supported AV1 bit depth, chroma format, range, matrix, transfer function, color primary, and chroma position, with scalar fallback only when hardware vectorization is unavailable or the operation is inherently non-vectorizable.
- [x] Apply ICC conversion only after the SIMD YUV/CICP stage, alpha composition, grid assembly, and presentation transforms have produced the presented RGB image; retain ImageSharp's shared ICC converter and optimize reusable bulk kernels rather than creating a HEIF-specific color-management implementation.
- [ ] Verify ICC preservation, conversion, compaction, and metadata skipping for grids, alpha-composited images, every presented sequence frame, and the completed HEVC path.
- [x] Independently encode AVIF primary, grid, alpha-auxiliary, and two-frame sequence vectors with libavif 1.4.2/libaom 3.14.1; verify exact profile preservation, non-sRGB conversion, canonical-sRGB compaction, non-sRGB compact preservation, metadata skipping, every presented frame, and unchanged composed alpha.
- [ ] Repeat the same matrix for HEVC primary, grid, alpha-auxiliary, and sequence paths after HEVC reconstruction is complete.
- [ ] Benchmark representative 8/10/12-bit end-to-end AVIF and HEIC color pipelines with and without ICC conversion, recording absolute timings and allocations before marking Phase 5 complete.
- [x] Add permanent frame-wide `Av1ColorConversionBenchmarks` coverage for sequential 1920x1080 YUV 4:2:0 conversion in both directions at 8, 10, and 12 bits, including managed-allocation reporting.
- [x] Record the .NET 10 short-run AV1 baseline on 2026-08-25. SIMD decode measured 4.025/2.803/2.742 ms and encode measured 3.750/2.555/2.450 ms at 8/10/12 bits with 488-744 B allocated per frame. Forced-scalar decode measured 31.15/29.47/28.40 ms and encode measured 19.19/15.77/15.14 ms, making the normal SIMD pipeline 7.7-10.5 times faster for decode and 5.1-6.2 times faster for encode on this machine.
- [ ] Add representative ICC-enabled AVIF cases and the equivalent HEVC cases after the HEVC reconstruction path is complete.
- [x] **Completed:** remove production `ToArray()` materializations from the HEIF implementation and make every retained copy an explicit ownership decision.
- [x] Store AV1 palette colors and HEVC short-term reference-picture state inline at their normative fixed bounds.
- [x] Retain only the AV1 configuration sequence-header payload required for item/sample equivalence instead of every configuration OBU.
@ -458,9 +465,17 @@ Exit gate:
Tasks:
- [ ] Implement monochrome, 4:2:0, 4:2:2, and 4:4:4 plane access with every valid signaled chroma sample position.
- [x] Implement SIMD AV1 4:2:0 and 4:2:2 chroma reconstruction and downsampling, including centered/colocated reconstruction and odd image edges.
- [ ] Complete the HEVC plane paths and the exhaustive AV1/HEVC chroma-position matrix before closing this item.
- [ ] Implement full- and limited-range expansion for 8, 10, and 12-bit samples across every supported plane layout.
- [x] Implement AV1 range expansion and quantization with SIMD-first byte/ushort loaders and storers and pooled planar rows.
- [ ] Complete the equivalent HEVC paths and independently verify the full cross-product.
- [ ] Implement every non-reserved HEVC/AV1 color-primary, transfer-characteristic, and matrix-coefficient signaling path, including identity conversion, with correct fixed-point rounding and clipping.
- [x] Implement the AV1 H.273 matrix and transfer-function operators in both directions, including identity, derived coefficients, constant luminance, SMPTE ST 2085, and PQ/HLG ICtCp.
- [ ] Add independent conformance vectors for signaling combinations that libavif deliberately rejects, then complete and verify the HEVC operator set.
- [ ] Make SIMD the default decode path for YUV range expansion, chroma reconstruction, H.273 matrix and transfer conversion, clamping, and 8/10/12-bit RGB packing. Use the established static operator pattern with `Vector512`, `Vector256`, and `Vector128` dispatch followed by one scalar tail that remains the behavioral oracle.
- [x] AV1 uses the JPEG-style static operator contract and descending SIMD-width traversal for decode and encode, with scalar execution limited to the remaining lanes or hardware without vector support.
- [ ] Complete the same default SIMD pipeline for HEVC and record end-to-end measurements for both codecs.
- [ ] Apply embedded ICC profiles after codec YUV/CICP conversion and image composition through ImageSharp's existing color-profile converter. Cover preserve, convert, compact, and ignore behavior for primary items, grids, alpha-composited images, and every presented sequence frame; never substitute CICP or an assumed sRGB profile for an embedded ICC profile.
- Primary AVIF verification passes four independently scoped cases: exact profile preservation, non-sRGB conversion with changed pixels and paired-source comparison, canonical-sRGB compaction without pixel changes, and metadata skipping. Grid, auxiliary-alpha, sequence, and HEVC cases remain open.
- [ ] Decode alpha auxiliary items as monochrome planes, validate dimensions and bit depth, and compose them without losing precision. Define premultiplication behavior from AVIF signaling and ImageSharp's pixel contract.
@ -479,6 +494,8 @@ Delete the JPEG payload path from the production encoder. Keep the synchronous I
Implement in vertical slices that always produce a decodable AV1 bitstream:
- [ ] Convert RGB/RGBA to AV1 planes for every 8, 10, and 12-bit output, range, matrix, and monochrome/4:2:0/4:2:2/4:4:4 combination permitted by the selected AV1 profile.
- [x] Implement the pooled sequential RGB-to-planar pipeline, 8/10/12-bit quantization, every implemented H.273 operator, and SIMD 4:2:0/4:2:2 downsampling with odd-edge handling.
- [ ] Complete the exhaustive format/range/matrix/transfer/primary cross-product and validate encoded planes against an independent implementation before closing this item.
- [ ] Write sequence, frame, tile-group, and metadata OBUs for a reduced still picture.
- [ ] Implement a temporary smallest-valid intra-only vertical slice using existing partition, prediction, transform, quantization, coefficient, and entropy structures.
- [ ] Complete block geometry, neighbor/context updates, transform selection and forward transforms, quantization, coefficient tokenization, and range coding.

155
src/ImageSharp/Formats/Heif/Av1/Av1ColorConversionParameters.cs

@ -0,0 +1,155 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Stores the resolved H.273 values shared by every scalar and SIMD lane.
/// </summary>
internal readonly struct Av1ColorConversionParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="Av1ColorConversionParameters"/> struct.
/// </summary>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kg">The green luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="constantLuminanceScales">The constant-luminance chroma scales.</param>
/// <param name="lumaBias">The encoded luma bias.</param>
/// <param name="lumaScale">The encoded luma range.</param>
/// <param name="chromaBias">The encoded chroma midpoint.</param>
/// <param name="chromaScale">The encoded chroma range.</param>
public Av1ColorConversionParameters(
float kr,
float kg,
float kb,
ObuTransferCharacteristics transferCharacteristics,
in Av1ConstantLuminanceScales constantLuminanceScales,
float lumaBias,
float lumaScale,
float chromaBias,
float chromaScale)
{
this.Kr = kr;
this.Kg = kg;
this.Kb = kb;
this.RedChromaScale = 2F * (1F - kr);
this.BlueChromaScale = 2F * (1F - kb);
this.GreenRedChromaScale = 2F * kr * (1F - kr) / kg;
this.GreenBlueChromaScale = 2F * kb * (1F - kb) / kg;
this.TransferCharacteristics = transferCharacteristics;
this.ConstantLuminanceScales = constantLuminanceScales;
this.LumaBias = lumaBias;
this.LumaScale = lumaScale;
this.ChromaBias = chromaBias;
this.ChromaScale = chromaScale;
}
/// <summary>
/// Gets the red luma coefficient.
/// </summary>
public float Kr { get; }
/// <summary>
/// Gets the green luma coefficient.
/// </summary>
public float Kg { get; }
/// <summary>
/// Gets the blue luma coefficient.
/// </summary>
public float Kb { get; }
/// <summary>
/// Gets the red contribution from the red-difference component.
/// </summary>
public float RedChromaScale { get; }
/// <summary>
/// Gets the blue contribution from the blue-difference component.
/// </summary>
public float BlueChromaScale { get; }
/// <summary>
/// Gets the red-difference subtraction from green.
/// </summary>
public float GreenRedChromaScale { get; }
/// <summary>
/// Gets the blue-difference subtraction from green.
/// </summary>
public float GreenBlueChromaScale { get; }
/// <summary>
/// Gets the signaled transfer characteristics.
/// </summary>
public ObuTransferCharacteristics TransferCharacteristics { get; }
/// <summary>
/// Gets the constant-luminance chroma scales.
/// </summary>
public Av1ConstantLuminanceScales ConstantLuminanceScales { get; }
/// <summary>
/// Gets the encoded luma bias.
/// </summary>
public float LumaBias { get; }
/// <summary>
/// Gets the encoded luma range.
/// </summary>
public float LumaScale { get; }
/// <summary>
/// Gets the encoded chroma midpoint.
/// </summary>
public float ChromaBias { get; }
/// <summary>
/// Gets the encoded chroma range.
/// </summary>
public float ChromaScale { get; }
}
/// <summary>
/// Stores the H.273 chroma normalization constants for constant-luminance conversion.
/// </summary>
internal readonly struct Av1ConstantLuminanceScales
{
/// <summary>
/// Initializes a new instance of the <see cref="Av1ConstantLuminanceScales"/> struct.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
public Av1ConstantLuminanceScales(ObuTransferCharacteristics transferCharacteristics, float kr, float kb)
{
this.NegativeBlue = Av1TransferFunctions.ToGamma(transferCharacteristics, 1F - kb);
this.PositiveBlue = 1F - Av1TransferFunctions.ToGamma(transferCharacteristics, kb);
this.NegativeRed = Av1TransferFunctions.ToGamma(transferCharacteristics, 1F - kr);
this.PositiveRed = 1F - Av1TransferFunctions.ToGamma(transferCharacteristics, kr);
}
/// <summary>
/// Gets the negative blue-difference scale.
/// </summary>
public float NegativeBlue { get; }
/// <summary>
/// Gets the positive blue-difference scale.
/// </summary>
public float PositiveBlue { get; }
/// <summary>
/// Gets the negative red-difference scale.
/// </summary>
public float NegativeRed { get; }
/// <summary>
/// Gets the positive red-difference scale.
/// </summary>
public float PositiveRed { get; }
}

156
src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.CoefficientOperator.cs

@ -0,0 +1,156 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
internal abstract partial class Av1ColorConverterBase
{
/// <summary>
/// Implements coefficient-based YCbCr conversion for scalar and SIMD lanes.
/// </summary>
internal readonly struct Av1CoefficientColorOperator : IAv1ColorOperator
{
/// <inheritdoc/>
public static bool ChromaUsesLumaRange => false;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref float y, ref float cb, ref float cr, in Av1ColorConversionParameters parameters)
{
// The resolved coefficients are frame invariants. Keeping the operator in this direct matrix form
// lets every SIMD overload map the same three equations to fused multiply-add instructions.
float r = y + (parameters.RedChromaScale * cr);
float g = y - (parameters.GreenRedChromaScale * cr) - (parameters.GreenBlueChromaScale * cb);
float b = y + (parameters.BlueChromaScale * cb);
y = r;
cb = g;
cr = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in Av1ColorConversionParameters parameters)
{
Vector128<float> r = Vector128.MultiplyAddEstimate(Vector128.Create(parameters.RedChromaScale), cr, y);
Vector128<float> g = Vector128.MultiplyAddEstimate(
Vector128.Create(-parameters.GreenBlueChromaScale),
cb,
Vector128.MultiplyAddEstimate(Vector128.Create(-parameters.GreenRedChromaScale), cr, y));
Vector128<float> b = Vector128.MultiplyAddEstimate(Vector128.Create(parameters.BlueChromaScale), cb, y);
y = r;
cb = g;
cr = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in Av1ColorConversionParameters parameters)
{
Vector256<float> r = Vector256.MultiplyAddEstimate(Vector256.Create(parameters.RedChromaScale), cr, y);
Vector256<float> g = Vector256.MultiplyAddEstimate(
Vector256.Create(-parameters.GreenBlueChromaScale),
cb,
Vector256.MultiplyAddEstimate(Vector256.Create(-parameters.GreenRedChromaScale), cr, y));
Vector256<float> b = Vector256.MultiplyAddEstimate(Vector256.Create(parameters.BlueChromaScale), cb, y);
y = r;
cb = g;
cr = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in Av1ColorConversionParameters parameters)
{
Vector512<float> r = Vector512.MultiplyAddEstimate(Vector512.Create(parameters.RedChromaScale), cr, y);
Vector512<float> g = Vector512.MultiplyAddEstimate(
Vector512.Create(-parameters.GreenBlueChromaScale),
cb,
Vector512.MultiplyAddEstimate(Vector512.Create(-parameters.GreenRedChromaScale), cr, y));
Vector512<float> b = Vector512.MultiplyAddEstimate(Vector512.Create(parameters.BlueChromaScale), cb, y);
y = r;
cb = g;
cr = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
float r,
float g,
float b,
in Av1ColorConversionParameters parameters,
out float y,
out float cb,
out float cr)
{
// Luma is shared by both chroma equations, so calculate it once before projecting blue and red.
y = (parameters.Kr * r) + (parameters.Kg * g) + (parameters.Kb * b);
cb = (b - y) / parameters.BlueChromaScale;
cr = (r - y) / parameters.RedChromaScale;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> r,
Vector128<float> g,
Vector128<float> b,
in Av1ColorConversionParameters parameters,
out Vector128<float> y,
out Vector128<float> cb,
out Vector128<float> cr)
{
y = Vector128.MultiplyAddEstimate(
Vector128.Create(parameters.Kr),
r,
Vector128.MultiplyAddEstimate(Vector128.Create(parameters.Kg), g, Vector128.Create(parameters.Kb) * b));
cb = (b - y) / Vector128.Create(parameters.BlueChromaScale);
cr = (r - y) / Vector128.Create(parameters.RedChromaScale);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> r,
Vector256<float> g,
Vector256<float> b,
in Av1ColorConversionParameters parameters,
out Vector256<float> y,
out Vector256<float> cb,
out Vector256<float> cr)
{
y = Vector256.MultiplyAddEstimate(
Vector256.Create(parameters.Kr),
r,
Vector256.MultiplyAddEstimate(Vector256.Create(parameters.Kg), g, Vector256.Create(parameters.Kb) * b));
cb = (b - y) / Vector256.Create(parameters.BlueChromaScale);
cr = (r - y) / Vector256.Create(parameters.RedChromaScale);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> r,
Vector512<float> g,
Vector512<float> b,
in Av1ColorConversionParameters parameters,
out Vector512<float> y,
out Vector512<float> cb,
out Vector512<float> cr)
{
y = Vector512.MultiplyAddEstimate(
Vector512.Create(parameters.Kr),
r,
Vector512.MultiplyAddEstimate(Vector512.Create(parameters.Kg), g, Vector512.Create(parameters.Kb) * b));
cb = (b - y) / Vector512.Create(parameters.BlueChromaScale);
cr = (r - y) / Vector512.Create(parameters.RedChromaScale);
}
}
}

249
src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.ConstantLuminanceOperator.cs

@ -0,0 +1,249 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
internal abstract partial class Av1ColorConverterBase
{
/// <summary>
/// Implements H.273 constant-luminance conversion for scalar and SIMD lanes.
/// </summary>
internal readonly struct Av1ConstantLuminanceColorOperator : IAv1ColorOperator
{
/// <inheritdoc/>
public static bool ChromaUsesLumaRange => false;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref float y, ref float cb, ref float cr, in Av1ColorConversionParameters parameters)
{
Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales;
// Constant-luminance chroma has different positive and negative divisors. Reconstruct nonlinear
// red and blue first, then solve for green in linear light using the signaled transfer curve.
float nonlinearBlue = y + (2F * (cb <= 0F ? scales.NegativeBlue : scales.PositiveBlue) * cb);
float nonlinearRed = y + (2F * (cr <= 0F ? scales.NegativeRed : scales.PositiveRed) * cr);
float linearY = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, y);
float linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue);
float linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed);
float linearGreen = (linearY - (parameters.Kr * linearRed) - (parameters.Kb * linearBlue)) / parameters.Kg;
y = nonlinearRed;
cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
cr = nonlinearBlue;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in Av1ColorConversionParameters parameters)
{
Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales;
Vector128<float> blueScale = Vector128.ConditionalSelect(
Vector128.LessThanOrEqual(cb, Vector128<float>.Zero),
Vector128.Create(scales.NegativeBlue),
Vector128.Create(scales.PositiveBlue));
Vector128<float> redScale = Vector128.ConditionalSelect(
Vector128.LessThanOrEqual(cr, Vector128<float>.Zero),
Vector128.Create(scales.NegativeRed),
Vector128.Create(scales.PositiveRed));
Vector128<float> nonlinearBlue = Vector128.MultiplyAddEstimate(Vector128.Create(2F) * blueScale, cb, y);
Vector128<float> nonlinearRed = Vector128.MultiplyAddEstimate(Vector128.Create(2F) * redScale, cr, y);
Vector128<float> linearY = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, y);
Vector128<float> linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue);
Vector128<float> linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed);
Vector128<float> linearGreen = (
linearY - (Vector128.Create(parameters.Kr) * linearRed) - (Vector128.Create(parameters.Kb) * linearBlue))
/ Vector128.Create(parameters.Kg);
y = nonlinearRed;
cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
cr = nonlinearBlue;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in Av1ColorConversionParameters parameters)
{
Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales;
Vector256<float> blueScale = Vector256.ConditionalSelect(
Vector256.LessThanOrEqual(cb, Vector256<float>.Zero),
Vector256.Create(scales.NegativeBlue),
Vector256.Create(scales.PositiveBlue));
Vector256<float> redScale = Vector256.ConditionalSelect(
Vector256.LessThanOrEqual(cr, Vector256<float>.Zero),
Vector256.Create(scales.NegativeRed),
Vector256.Create(scales.PositiveRed));
Vector256<float> nonlinearBlue = Vector256.MultiplyAddEstimate(Vector256.Create(2F) * blueScale, cb, y);
Vector256<float> nonlinearRed = Vector256.MultiplyAddEstimate(Vector256.Create(2F) * redScale, cr, y);
Vector256<float> linearY = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, y);
Vector256<float> linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue);
Vector256<float> linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed);
Vector256<float> linearGreen = (
linearY - (Vector256.Create(parameters.Kr) * linearRed) - (Vector256.Create(parameters.Kb) * linearBlue))
/ Vector256.Create(parameters.Kg);
y = nonlinearRed;
cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
cr = nonlinearBlue;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in Av1ColorConversionParameters parameters)
{
Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales;
Vector512<float> blueScale = Vector512.ConditionalSelect(
Vector512.LessThanOrEqual(cb, Vector512<float>.Zero),
Vector512.Create(scales.NegativeBlue),
Vector512.Create(scales.PositiveBlue));
Vector512<float> redScale = Vector512.ConditionalSelect(
Vector512.LessThanOrEqual(cr, Vector512<float>.Zero),
Vector512.Create(scales.NegativeRed),
Vector512.Create(scales.PositiveRed));
Vector512<float> nonlinearBlue = Vector512.MultiplyAddEstimate(Vector512.Create(2F) * blueScale, cb, y);
Vector512<float> nonlinearRed = Vector512.MultiplyAddEstimate(Vector512.Create(2F) * redScale, cr, y);
Vector512<float> linearY = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, y);
Vector512<float> linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue);
Vector512<float> linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed);
Vector512<float> linearGreen = (
linearY - (Vector512.Create(parameters.Kr) * linearRed) - (Vector512.Create(parameters.Kb) * linearBlue))
/ Vector512.Create(parameters.Kg);
y = nonlinearRed;
cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
cr = nonlinearBlue;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
float r,
float g,
float b,
in Av1ColorConversionParameters parameters,
out float y,
out float cb,
out float cr)
{
// Luma is formed in linear light. The nonlinear red and blue differences then choose the
// sign-dependent denominators that define constant-luminance Cb and Cr.
float linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, r);
float linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, g);
float linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, b);
float linearY = (parameters.Kr * linearRed) + (parameters.Kg * linearGreen) + (parameters.Kb * linearBlue);
y = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY);
float blueDifference = b - y;
float redDifference = r - y;
Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales;
cb = blueDifference / (2F * (blueDifference <= 0F ? scales.NegativeBlue : scales.PositiveBlue));
cr = redDifference / (2F * (redDifference <= 0F ? scales.NegativeRed : scales.PositiveRed));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> r,
Vector128<float> g,
Vector128<float> b,
in Av1ColorConversionParameters parameters,
out Vector128<float> y,
out Vector128<float> cb,
out Vector128<float> cr)
{
Vector128<float> linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, r);
Vector128<float> linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, g);
Vector128<float> linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, b);
Vector128<float> linearY = Vector128.MultiplyAddEstimate(
Vector128.Create(parameters.Kr),
linearRed,
Vector128.MultiplyAddEstimate(Vector128.Create(parameters.Kg), linearGreen, Vector128.Create(parameters.Kb) * linearBlue));
y = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY);
Vector128<float> blueDifference = b - y;
Vector128<float> redDifference = r - y;
Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales;
Vector128<float> blueScale = Vector128.ConditionalSelect(
Vector128.LessThanOrEqual(blueDifference, Vector128<float>.Zero),
Vector128.Create(scales.NegativeBlue),
Vector128.Create(scales.PositiveBlue));
Vector128<float> redScale = Vector128.ConditionalSelect(
Vector128.LessThanOrEqual(redDifference, Vector128<float>.Zero),
Vector128.Create(scales.NegativeRed),
Vector128.Create(scales.PositiveRed));
cb = blueDifference / (Vector128.Create(2F) * blueScale);
cr = redDifference / (Vector128.Create(2F) * redScale);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> r,
Vector256<float> g,
Vector256<float> b,
in Av1ColorConversionParameters parameters,
out Vector256<float> y,
out Vector256<float> cb,
out Vector256<float> cr)
{
Vector256<float> linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, r);
Vector256<float> linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, g);
Vector256<float> linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, b);
Vector256<float> linearY = Vector256.MultiplyAddEstimate(
Vector256.Create(parameters.Kr),
linearRed,
Vector256.MultiplyAddEstimate(Vector256.Create(parameters.Kg), linearGreen, Vector256.Create(parameters.Kb) * linearBlue));
y = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY);
Vector256<float> blueDifference = b - y;
Vector256<float> redDifference = r - y;
Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales;
Vector256<float> blueScale = Vector256.ConditionalSelect(
Vector256.LessThanOrEqual(blueDifference, Vector256<float>.Zero),
Vector256.Create(scales.NegativeBlue),
Vector256.Create(scales.PositiveBlue));
Vector256<float> redScale = Vector256.ConditionalSelect(
Vector256.LessThanOrEqual(redDifference, Vector256<float>.Zero),
Vector256.Create(scales.NegativeRed),
Vector256.Create(scales.PositiveRed));
cb = blueDifference / (Vector256.Create(2F) * blueScale);
cr = redDifference / (Vector256.Create(2F) * redScale);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> r,
Vector512<float> g,
Vector512<float> b,
in Av1ColorConversionParameters parameters,
out Vector512<float> y,
out Vector512<float> cb,
out Vector512<float> cr)
{
Vector512<float> linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, r);
Vector512<float> linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, g);
Vector512<float> linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, b);
Vector512<float> linearY = Vector512.MultiplyAddEstimate(
Vector512.Create(parameters.Kr),
linearRed,
Vector512.MultiplyAddEstimate(Vector512.Create(parameters.Kg), linearGreen, Vector512.Create(parameters.Kb) * linearBlue));
y = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY);
Vector512<float> blueDifference = b - y;
Vector512<float> redDifference = r - y;
Av1ConstantLuminanceScales scales = parameters.ConstantLuminanceScales;
Vector512<float> blueScale = Vector512.ConditionalSelect(
Vector512.LessThanOrEqual(blueDifference, Vector512<float>.Zero),
Vector512.Create(scales.NegativeBlue),
Vector512.Create(scales.PositiveBlue));
Vector512<float> redScale = Vector512.ConditionalSelect(
Vector512.LessThanOrEqual(redDifference, Vector512<float>.Zero),
Vector512.Create(scales.NegativeRed),
Vector512.Create(scales.PositiveRed));
cb = blueDifference / (Vector512.Create(2F) * blueScale);
cr = redDifference / (Vector512.Create(2F) * redScale);
}
}
}

526
src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.ICtCpOperator.cs

@ -0,0 +1,526 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
internal abstract partial class Av1ColorConverterBase
{
/// <summary>
/// Implements BT.2100 ICtCp conversion for scalar and SIMD lanes.
/// </summary>
internal readonly struct Av1ICtCpColorOperator : IAv1ColorOperator
{
/// <summary>
/// The PQ Ct contribution to nonlinear L.
/// </summary>
public const float PqCtToL = 0.008609037037932756F;
/// <summary>
/// The PQ Cp contribution to nonlinear L.
/// </summary>
public const float PqCpToL = 0.11102962500302596F;
/// <summary>
/// The PQ Ct contribution to nonlinear S.
/// </summary>
public const float PqCtToS = 0.5600313357106791F;
/// <summary>
/// The PQ Cp contribution to nonlinear S.
/// </summary>
public const float PqCpToS = -0.32062717498731885F;
/// <summary>
/// The HLG Ct contribution to nonlinear L.
/// </summary>
public const float HlgCtToL = 0.015718580108730413F;
/// <summary>
/// The HLG Cp contribution to nonlinear L.
/// </summary>
public const float HlgCpToL = 0.2095810681164055F;
/// <summary>
/// The HLG Ct contribution to nonlinear S.
/// </summary>
public const float HlgCtToS = 1.0212710798422342F;
/// <summary>
/// The HLG Cp contribution to nonlinear S.
/// </summary>
public const float HlgCpToS = -0.6052744909924315F;
/// <summary>
/// The linear L contribution to red.
/// </summary>
public const float LToRed = 3.4366066943330784F;
/// <summary>
/// The linear M contribution to red.
/// </summary>
public const float MToRed = -2.50645211865627F;
/// <summary>
/// The linear S contribution to red.
/// </summary>
public const float SToRed = 0.06984542432319148F;
/// <summary>
/// The linear L contribution to green.
/// </summary>
public const float LToGreen = -0.7913295555989287F;
/// <summary>
/// The linear M contribution to green.
/// </summary>
public const float MToGreen = 1.9836004517922907F;
/// <summary>
/// The linear S contribution to green.
/// </summary>
public const float SToGreen = -0.192270896193362F;
/// <summary>
/// The linear L contribution to blue.
/// </summary>
public const float LToBlue = -0.025949899690592672F;
/// <summary>
/// The linear M contribution to blue.
/// </summary>
public const float MToBlue = -0.09891371471172644F;
/// <summary>
/// The linear S contribution to blue.
/// </summary>
public const float SToBlue = 1.1248636144023192F;
/// <summary>
/// The linear red contribution to L.
/// </summary>
public const float RedToL = 1688F / 4096F;
/// <summary>
/// The linear green contribution to L.
/// </summary>
public const float GreenToL = 2146F / 4096F;
/// <summary>
/// The linear blue contribution to L.
/// </summary>
public const float BlueToL = 262F / 4096F;
/// <summary>
/// The linear red contribution to M.
/// </summary>
public const float RedToM = 683F / 4096F;
/// <summary>
/// The linear green contribution to M.
/// </summary>
public const float GreenToM = 2951F / 4096F;
/// <summary>
/// The linear blue contribution to M.
/// </summary>
public const float BlueToM = 462F / 4096F;
/// <summary>
/// The linear red contribution to S.
/// </summary>
public const float RedToS = 99F / 4096F;
/// <summary>
/// The linear green contribution to S.
/// </summary>
public const float GreenToS = 309F / 4096F;
/// <summary>
/// The linear blue contribution to S.
/// </summary>
public const float BlueToS = 3688F / 4096F;
/// <summary>
/// The PQ nonlinear L contribution to Ct.
/// </summary>
public const float PqLToCt = 6610F / 4096F;
/// <summary>
/// The PQ nonlinear M contribution to Ct.
/// </summary>
public const float PqMToCt = -13613F / 4096F;
/// <summary>
/// The PQ nonlinear S contribution to Ct.
/// </summary>
public const float PqSToCt = 7003F / 4096F;
/// <summary>
/// The PQ nonlinear L contribution to Cp.
/// </summary>
public const float PqLToCp = 17933F / 4096F;
/// <summary>
/// The PQ nonlinear M contribution to Cp.
/// </summary>
public const float PqMToCp = -17390F / 4096F;
/// <summary>
/// The PQ nonlinear S contribution to Cp.
/// </summary>
public const float PqSToCp = -543F / 4096F;
/// <summary>
/// The HLG nonlinear L contribution to Ct.
/// </summary>
public const float HlgLToCt = 3625F / 4096F;
/// <summary>
/// The HLG nonlinear M contribution to Ct.
/// </summary>
public const float HlgMToCt = -7465F / 4096F;
/// <summary>
/// The HLG nonlinear S contribution to Ct.
/// </summary>
public const float HlgSToCt = 3840F / 4096F;
/// <summary>
/// The HLG nonlinear L contribution to Cp.
/// </summary>
public const float HlgLToCp = 9500F / 4096F;
/// <summary>
/// The HLG nonlinear M contribution to Cp.
/// </summary>
public const float HlgMToCp = -9212F / 4096F;
/// <summary>
/// The HLG nonlinear S contribution to Cp.
/// </summary>
public const float HlgSToCp = -288F / 4096F;
/// <inheritdoc/>
public static bool ChromaUsesLumaRange => false;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref float intensity, ref float ct, ref float cp, in Av1ColorConversionParameters parameters)
{
bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg;
float ctToL = isHlg ? HlgCtToL : PqCtToL;
float cpToL = isHlg ? HlgCpToL : PqCpToL;
// The inverse ICtCp matrix first reconstructs nonlinear LMS. The transfer curve is then
// removed before the fixed LMS-to-RGB matrix and reapplied to the three output primaries.
float nonlinearL = intensity + (ctToL * ct) + (cpToL * cp);
float nonlinearM = intensity - (ctToL * ct) - (cpToL * cp);
float nonlinearS = intensity + ((isHlg ? HlgCtToS : PqCtToS) * ct) + ((isHlg ? HlgCpToS : PqCpToS) * cp);
float linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL);
float linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM);
float linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS);
float linearRed = (LToRed * linearL) + (MToRed * linearM) + (SToRed * linearS);
float linearGreen = (LToGreen * linearL) + (MToGreen * linearM) + (SToGreen * linearS);
float linearBlue = (LToBlue * linearL) + (MToBlue * linearM) + (SToBlue * linearS);
intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed);
ct = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
cp = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector128<float> intensity,
ref Vector128<float> ct,
ref Vector128<float> cp,
in Av1ColorConversionParameters parameters)
{
bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg;
Vector128<float> ctContribution = Vector128.Create(isHlg ? HlgCtToL : PqCtToL) * ct;
Vector128<float> cpContribution = Vector128.Create(isHlg ? HlgCpToL : PqCpToL) * cp;
Vector128<float> nonlinearL = intensity + ctContribution + cpContribution;
Vector128<float> nonlinearM = intensity - ctContribution - cpContribution;
Vector128<float> nonlinearS = Vector128.MultiplyAddEstimate(
Vector128.Create(isHlg ? HlgCpToS : PqCpToS),
cp,
Vector128.MultiplyAddEstimate(Vector128.Create(isHlg ? HlgCtToS : PqCtToS), ct, intensity));
Vector128<float> linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL);
Vector128<float> linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM);
Vector128<float> linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS);
Vector128<float> linearRed = Vector128.MultiplyAddEstimate(
Vector128.Create(SToRed),
linearS,
Vector128.MultiplyAddEstimate(Vector128.Create(MToRed), linearM, Vector128.Create(LToRed) * linearL));
Vector128<float> linearGreen = Vector128.MultiplyAddEstimate(
Vector128.Create(SToGreen),
linearS,
Vector128.MultiplyAddEstimate(Vector128.Create(MToGreen), linearM, Vector128.Create(LToGreen) * linearL));
Vector128<float> linearBlue = Vector128.MultiplyAddEstimate(
Vector128.Create(SToBlue),
linearS,
Vector128.MultiplyAddEstimate(Vector128.Create(MToBlue), linearM, Vector128.Create(LToBlue) * linearL));
intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed);
ct = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
cp = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector256<float> intensity,
ref Vector256<float> ct,
ref Vector256<float> cp,
in Av1ColorConversionParameters parameters)
{
bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg;
Vector256<float> ctContribution = Vector256.Create(isHlg ? HlgCtToL : PqCtToL) * ct;
Vector256<float> cpContribution = Vector256.Create(isHlg ? HlgCpToL : PqCpToL) * cp;
Vector256<float> nonlinearL = intensity + ctContribution + cpContribution;
Vector256<float> nonlinearM = intensity - ctContribution - cpContribution;
Vector256<float> nonlinearS = Vector256.MultiplyAddEstimate(
Vector256.Create(isHlg ? HlgCpToS : PqCpToS),
cp,
Vector256.MultiplyAddEstimate(Vector256.Create(isHlg ? HlgCtToS : PqCtToS), ct, intensity));
Vector256<float> linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL);
Vector256<float> linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM);
Vector256<float> linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS);
Vector256<float> linearRed = Vector256.MultiplyAddEstimate(
Vector256.Create(SToRed),
linearS,
Vector256.MultiplyAddEstimate(Vector256.Create(MToRed), linearM, Vector256.Create(LToRed) * linearL));
Vector256<float> linearGreen = Vector256.MultiplyAddEstimate(
Vector256.Create(SToGreen),
linearS,
Vector256.MultiplyAddEstimate(Vector256.Create(MToGreen), linearM, Vector256.Create(LToGreen) * linearL));
Vector256<float> linearBlue = Vector256.MultiplyAddEstimate(
Vector256.Create(SToBlue),
linearS,
Vector256.MultiplyAddEstimate(Vector256.Create(MToBlue), linearM, Vector256.Create(LToBlue) * linearL));
intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed);
ct = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
cp = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector512<float> intensity,
ref Vector512<float> ct,
ref Vector512<float> cp,
in Av1ColorConversionParameters parameters)
{
bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg;
Vector512<float> ctContribution = Vector512.Create(isHlg ? HlgCtToL : PqCtToL) * ct;
Vector512<float> cpContribution = Vector512.Create(isHlg ? HlgCpToL : PqCpToL) * cp;
Vector512<float> nonlinearL = intensity + ctContribution + cpContribution;
Vector512<float> nonlinearM = intensity - ctContribution - cpContribution;
Vector512<float> nonlinearS = Vector512.MultiplyAddEstimate(
Vector512.Create(isHlg ? HlgCpToS : PqCpToS),
cp,
Vector512.MultiplyAddEstimate(Vector512.Create(isHlg ? HlgCtToS : PqCtToS), ct, intensity));
Vector512<float> linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL);
Vector512<float> linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM);
Vector512<float> linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS);
Vector512<float> linearRed = Vector512.MultiplyAddEstimate(
Vector512.Create(SToRed),
linearS,
Vector512.MultiplyAddEstimate(Vector512.Create(MToRed), linearM, Vector512.Create(LToRed) * linearL));
Vector512<float> linearGreen = Vector512.MultiplyAddEstimate(
Vector512.Create(SToGreen),
linearS,
Vector512.MultiplyAddEstimate(Vector512.Create(MToGreen), linearM, Vector512.Create(LToGreen) * linearL));
Vector512<float> linearBlue = Vector512.MultiplyAddEstimate(
Vector512.Create(SToBlue),
linearS,
Vector512.MultiplyAddEstimate(Vector512.Create(MToBlue), linearM, Vector512.Create(LToBlue) * linearL));
intensity = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed);
ct = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
cp = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
float red,
float green,
float blue,
in Av1ColorConversionParameters parameters,
out float intensity,
out float ct,
out float cp)
{
// ICtCp is defined in nonlinear LMS. Convert RGB to linear light, apply the LMS matrix, then
// apply the signaled PQ or HLG transfer curve before deriving intensity and the chroma axes.
float linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red);
float linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green);
float linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue);
float nonlinearL = Av1TransferFunctions.ToGamma(
parameters.TransferCharacteristics,
(RedToL * linearRed) + (GreenToL * linearGreen) + (BlueToL * linearBlue));
float nonlinearM = Av1TransferFunctions.ToGamma(
parameters.TransferCharacteristics,
(RedToM * linearRed) + (GreenToM * linearGreen) + (BlueToM * linearBlue));
float nonlinearS = Av1TransferFunctions.ToGamma(
parameters.TransferCharacteristics,
(RedToS * linearRed) + (GreenToS * linearGreen) + (BlueToS * linearBlue));
intensity = 0.5F * (nonlinearL + nonlinearM);
bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg;
ct = isHlg
? (HlgLToCt * nonlinearL) + (HlgMToCt * nonlinearM) + (HlgSToCt * nonlinearS)
: (PqLToCt * nonlinearL) + (PqMToCt * nonlinearM) + (PqSToCt * nonlinearS);
cp = isHlg
? (HlgLToCp * nonlinearL) + (HlgMToCp * nonlinearM) + (HlgSToCp * nonlinearS)
: (PqLToCp * nonlinearL) + (PqMToCp * nonlinearM) + (PqSToCp * nonlinearS);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> red,
Vector128<float> green,
Vector128<float> blue,
in Av1ColorConversionParameters parameters,
out Vector128<float> intensity,
out Vector128<float> ct,
out Vector128<float> cp)
{
Vector128<float> linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red);
Vector128<float> linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green);
Vector128<float> linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue);
Vector128<float> linearL = Vector128.MultiplyAddEstimate(
Vector128.Create(RedToL),
linearRed,
Vector128.MultiplyAddEstimate(Vector128.Create(GreenToL), linearGreen, Vector128.Create(BlueToL) * linearBlue));
Vector128<float> linearM = Vector128.MultiplyAddEstimate(
Vector128.Create(RedToM),
linearRed,
Vector128.MultiplyAddEstimate(Vector128.Create(GreenToM), linearGreen, Vector128.Create(BlueToM) * linearBlue));
Vector128<float> linearS = Vector128.MultiplyAddEstimate(
Vector128.Create(RedToS),
linearRed,
Vector128.MultiplyAddEstimate(Vector128.Create(GreenToS), linearGreen, Vector128.Create(BlueToS) * linearBlue));
Vector128<float> nonlinearL = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL);
Vector128<float> nonlinearM = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM);
Vector128<float> nonlinearS = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS);
intensity = Vector128.Create(0.5F) * (nonlinearL + nonlinearM);
bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg;
float lToCt = isHlg ? HlgLToCt : PqLToCt;
float mToCt = isHlg ? HlgMToCt : PqMToCt;
float sToCt = isHlg ? HlgSToCt : PqSToCt;
float lToCp = isHlg ? HlgLToCp : PqLToCp;
float mToCp = isHlg ? HlgMToCp : PqMToCp;
float sToCp = isHlg ? HlgSToCp : PqSToCp;
ct = Vector128.MultiplyAddEstimate(
Vector128.Create(lToCt),
nonlinearL,
Vector128.MultiplyAddEstimate(Vector128.Create(mToCt), nonlinearM, Vector128.Create(sToCt) * nonlinearS));
cp = Vector128.MultiplyAddEstimate(
Vector128.Create(lToCp),
nonlinearL,
Vector128.MultiplyAddEstimate(Vector128.Create(mToCp), nonlinearM, Vector128.Create(sToCp) * nonlinearS));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> red,
Vector256<float> green,
Vector256<float> blue,
in Av1ColorConversionParameters parameters,
out Vector256<float> intensity,
out Vector256<float> ct,
out Vector256<float> cp)
{
Vector256<float> linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red);
Vector256<float> linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green);
Vector256<float> linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue);
Vector256<float> linearL = Vector256.MultiplyAddEstimate(
Vector256.Create(RedToL),
linearRed,
Vector256.MultiplyAddEstimate(Vector256.Create(GreenToL), linearGreen, Vector256.Create(BlueToL) * linearBlue));
Vector256<float> linearM = Vector256.MultiplyAddEstimate(
Vector256.Create(RedToM),
linearRed,
Vector256.MultiplyAddEstimate(Vector256.Create(GreenToM), linearGreen, Vector256.Create(BlueToM) * linearBlue));
Vector256<float> linearS = Vector256.MultiplyAddEstimate(
Vector256.Create(RedToS),
linearRed,
Vector256.MultiplyAddEstimate(Vector256.Create(GreenToS), linearGreen, Vector256.Create(BlueToS) * linearBlue));
Vector256<float> nonlinearL = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL);
Vector256<float> nonlinearM = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM);
Vector256<float> nonlinearS = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS);
intensity = Vector256.Create(0.5F) * (nonlinearL + nonlinearM);
bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg;
float lToCt = isHlg ? HlgLToCt : PqLToCt;
float mToCt = isHlg ? HlgMToCt : PqMToCt;
float sToCt = isHlg ? HlgSToCt : PqSToCt;
float lToCp = isHlg ? HlgLToCp : PqLToCp;
float mToCp = isHlg ? HlgMToCp : PqMToCp;
float sToCp = isHlg ? HlgSToCp : PqSToCp;
ct = Vector256.MultiplyAddEstimate(
Vector256.Create(lToCt),
nonlinearL,
Vector256.MultiplyAddEstimate(Vector256.Create(mToCt), nonlinearM, Vector256.Create(sToCt) * nonlinearS));
cp = Vector256.MultiplyAddEstimate(
Vector256.Create(lToCp),
nonlinearL,
Vector256.MultiplyAddEstimate(Vector256.Create(mToCp), nonlinearM, Vector256.Create(sToCp) * nonlinearS));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> red,
Vector512<float> green,
Vector512<float> blue,
in Av1ColorConversionParameters parameters,
out Vector512<float> intensity,
out Vector512<float> ct,
out Vector512<float> cp)
{
Vector512<float> linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, red);
Vector512<float> linearGreen = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, green);
Vector512<float> linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, blue);
Vector512<float> linearL = Vector512.MultiplyAddEstimate(
Vector512.Create(RedToL),
linearRed,
Vector512.MultiplyAddEstimate(Vector512.Create(GreenToL), linearGreen, Vector512.Create(BlueToL) * linearBlue));
Vector512<float> linearM = Vector512.MultiplyAddEstimate(
Vector512.Create(RedToM),
linearRed,
Vector512.MultiplyAddEstimate(Vector512.Create(GreenToM), linearGreen, Vector512.Create(BlueToM) * linearBlue));
Vector512<float> linearS = Vector512.MultiplyAddEstimate(
Vector512.Create(RedToS),
linearRed,
Vector512.MultiplyAddEstimate(Vector512.Create(GreenToS), linearGreen, Vector512.Create(BlueToS) * linearBlue));
Vector512<float> nonlinearL = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL);
Vector512<float> nonlinearM = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM);
Vector512<float> nonlinearS = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS);
intensity = Vector512.Create(0.5F) * (nonlinearL + nonlinearM);
bool isHlg = parameters.TransferCharacteristics == ObuTransferCharacteristics.Hlg;
float lToCt = isHlg ? HlgLToCt : PqLToCt;
float mToCt = isHlg ? HlgMToCt : PqMToCt;
float sToCt = isHlg ? HlgSToCt : PqSToCt;
float lToCp = isHlg ? HlgLToCp : PqLToCp;
float mToCp = isHlg ? HlgMToCp : PqMToCp;
float sToCp = isHlg ? HlgSToCp : PqSToCp;
ct = Vector512.MultiplyAddEstimate(
Vector512.Create(lToCt),
nonlinearL,
Vector512.MultiplyAddEstimate(Vector512.Create(mToCt), nonlinearM, Vector512.Create(sToCt) * nonlinearS));
cp = Vector512.MultiplyAddEstimate(
Vector512.Create(lToCp),
nonlinearL,
Vector512.MultiplyAddEstimate(Vector512.Create(mToCp), nonlinearM, Vector512.Create(sToCp) * nonlinearS));
}
}
}

138
src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.IdentityOperator.cs

@ -0,0 +1,138 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
internal abstract partial class Av1ColorConverterBase
{
/// <summary>
/// Implements direct G, B, and R plane mapping for scalar and SIMD lanes.
/// </summary>
internal readonly struct Av1IdentityColorOperator : IAv1ColorOperator
{
/// <inheritdoc/>
public static bool ChromaUsesLumaRange => true;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref float green, ref float blue, ref float red, in Av1ColorConversionParameters parameters)
{
// Identity-matrix AV1 stores the planes in G, B, R order. Rotate the three references in place
// so the shared traversal always leaves component0/component1/component2 as R, G, B.
float g = green;
green = red;
red = blue;
blue = g;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector128<float> green,
ref Vector128<float> blue,
ref Vector128<float> red,
in Av1ColorConversionParameters parameters)
{
Vector128<float> g = green;
green = red;
red = blue;
blue = g;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector256<float> green,
ref Vector256<float> blue,
ref Vector256<float> red,
in Av1ColorConversionParameters parameters)
{
Vector256<float> g = green;
green = red;
red = blue;
blue = g;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector512<float> green,
ref Vector512<float> blue,
ref Vector512<float> red,
in Av1ColorConversionParameters parameters)
{
Vector512<float> g = green;
green = red;
red = blue;
blue = g;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
float red,
float green,
float blue,
in Av1ColorConversionParameters parameters,
out float component0,
out float component1,
out float component2)
{
// Identity-matrix AV1 stores RGB input as G, B, R without matrix arithmetic.
component0 = green;
component1 = blue;
component2 = red;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> red,
Vector128<float> green,
Vector128<float> blue,
in Av1ColorConversionParameters parameters,
out Vector128<float> component0,
out Vector128<float> component1,
out Vector128<float> component2)
{
component0 = green;
component1 = blue;
component2 = red;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> red,
Vector256<float> green,
Vector256<float> blue,
in Av1ColorConversionParameters parameters,
out Vector256<float> component0,
out Vector256<float> component1,
out Vector256<float> component2)
{
component0 = green;
component1 = blue;
component2 = red;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> red,
Vector512<float> green,
Vector512<float> blue,
in Av1ColorConversionParameters parameters,
out Vector512<float> component0,
out Vector512<float> component1,
out Vector512<float> component2)
{
component0 = green;
component1 = blue;
component2 = red;
}
}
}

427
src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.Operator.cs

@ -0,0 +1,427 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <content>
/// Provides the static operator contract and SIMD traversal used by AV1 color converters.
/// </content>
internal abstract partial class Av1ColorConverterBase
{
/// <summary>
/// Defines color-model arithmetic for scalar and SIMD lanes in both conversion directions.
/// </summary>
internal interface IAv1ColorOperator
{
/// <summary>
/// Gets a value indicating whether chroma uses the luma range rather than the centered chroma range.
/// </summary>
public static abstract bool ChromaUsesLumaRange { get; }
/// <summary>
/// Converts one normalized AV1 sample to RGB.
/// </summary>
/// <param name="component0">The first encoded component, replaced by red.</param>
/// <param name="component1">The second encoded component, replaced by green.</param>
/// <param name="component2">The third encoded component, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void ConvertToRgb(
ref float component0,
ref float component1,
ref float component2,
in Av1ColorConversionParameters parameters);
/// <summary>
/// Converts four normalized AV1 samples to RGB.
/// </summary>
/// <param name="component0">The first encoded component lanes, replaced by red.</param>
/// <param name="component1">The second encoded component lanes, replaced by green.</param>
/// <param name="component2">The third encoded component lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void ConvertToRgb(
ref Vector128<float> component0,
ref Vector128<float> component1,
ref Vector128<float> component2,
in Av1ColorConversionParameters parameters);
/// <summary>
/// Converts eight normalized AV1 samples to RGB.
/// </summary>
/// <param name="component0">The first encoded component lanes, replaced by red.</param>
/// <param name="component1">The second encoded component lanes, replaced by green.</param>
/// <param name="component2">The third encoded component lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void ConvertToRgb(
ref Vector256<float> component0,
ref Vector256<float> component1,
ref Vector256<float> component2,
in Av1ColorConversionParameters parameters);
/// <summary>
/// Converts sixteen normalized AV1 samples to RGB.
/// </summary>
/// <param name="component0">The first encoded component lanes, replaced by red.</param>
/// <param name="component1">The second encoded component lanes, replaced by green.</param>
/// <param name="component2">The third encoded component lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void ConvertToRgb(
ref Vector512<float> component0,
ref Vector512<float> component1,
ref Vector512<float> component2,
in Av1ColorConversionParameters parameters);
/// <summary>
/// Converts one normalized RGB sample to AV1 components.
/// </summary>
/// <param name="red">The normalized red component.</param>
/// <param name="green">The normalized green component.</param>
/// <param name="blue">The normalized blue component.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="component0">The first converted component.</param>
/// <param name="component1">The second converted component.</param>
/// <param name="component2">The third converted component.</param>
public static abstract void ConvertFromRgb(
float red,
float green,
float blue,
in Av1ColorConversionParameters parameters,
out float component0,
out float component1,
out float component2);
/// <summary>
/// Converts four normalized RGB samples to AV1 components.
/// </summary>
/// <param name="red">The normalized red lanes.</param>
/// <param name="green">The normalized green lanes.</param>
/// <param name="blue">The normalized blue lanes.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="component0">The first converted component lanes.</param>
/// <param name="component1">The second converted component lanes.</param>
/// <param name="component2">The third converted component lanes.</param>
public static abstract void ConvertFromRgb(
Vector128<float> red,
Vector128<float> green,
Vector128<float> blue,
in Av1ColorConversionParameters parameters,
out Vector128<float> component0,
out Vector128<float> component1,
out Vector128<float> component2);
/// <summary>
/// Converts eight normalized RGB samples to AV1 components.
/// </summary>
/// <param name="red">The normalized red lanes.</param>
/// <param name="green">The normalized green lanes.</param>
/// <param name="blue">The normalized blue lanes.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="component0">The first converted component lanes.</param>
/// <param name="component1">The second converted component lanes.</param>
/// <param name="component2">The third converted component lanes.</param>
public static abstract void ConvertFromRgb(
Vector256<float> red,
Vector256<float> green,
Vector256<float> blue,
in Av1ColorConversionParameters parameters,
out Vector256<float> component0,
out Vector256<float> component1,
out Vector256<float> component2);
/// <summary>
/// Converts sixteen normalized RGB samples to AV1 components.
/// </summary>
/// <param name="red">The normalized red lanes.</param>
/// <param name="green">The normalized green lanes.</param>
/// <param name="blue">The normalized blue lanes.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="component0">The first converted component lanes.</param>
/// <param name="component1">The second converted component lanes.</param>
/// <param name="component2">The third converted component lanes.</param>
public static abstract void ConvertFromRgb(
Vector512<float> red,
Vector512<float> green,
Vector512<float> blue,
in Av1ColorConversionParameters parameters,
out Vector512<float> component0,
out Vector512<float> component1,
out Vector512<float> component2);
}
/// <summary>
/// Converts an AV1 color model using one operator-driven traversal for all SIMD widths.
/// </summary>
/// <typeparam name="TOperator">The color-model-specific arithmetic.</typeparam>
internal sealed class Av1ColorConverter<TOperator> : Av1ColorConverterBase
where TOperator : struct, IAv1ColorOperator
{
/// <summary>
/// Initializes a new instance of the <see cref="Av1ColorConverter{TOperator}"/> class.
/// </summary>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="isMonochrome">Whether the frame contains only luma samples.</param>
public Av1ColorConverter(in Av1ColorConversionParameters parameters, bool isMonochrome)
: base(in parameters, isMonochrome)
{
}
/// <inheritdoc/>
public override float ChromaScale => TOperator.ChromaUsesLumaRange ? this.Parameters.LumaScale : this.Parameters.ChromaScale;
/// <inheritdoc/>
public override float ChromaBias => TOperator.ChromaUsesLumaRange ? this.Parameters.LumaBias : this.Parameters.ChromaBias;
/// <inheritdoc/>
public override void ConvertToRgbInPlace(Span<float> component0, Span<float> component1, Span<float> component2)
{
Av1ColorConversionParameters parameters = this.Parameters;
// Row reconstruction owns equally sized planar buffers. As in JPEG, first-element byrefs let each
// SIMD width share one offset while the closed operator type keeps color-model dispatch out of the loop.
ref float component0Base = ref MemoryMarshal.GetReference(component0);
ref float component1Base = ref MemoryMarshal.GetReference(component1);
ref float component2Base = ref MemoryMarshal.GetReference(component2);
int length = component0.Length;
int i = 0;
if (this.IsMonochrome)
{
// Monochrome has no operator arithmetic: expanding the luma range once and copying each SIMD
// vector to all three planes is cheaper than routing it through a three-component operator.
if (Vector512.IsHardwareAccelerated && i <= length - Vector512<float>.Count)
{
Vector512<float> bias = Vector512.Create(parameters.LumaBias);
Vector512<float> inverseScale = Vector512.Create(1F / parameters.LumaScale);
int oneVectorFromEnd = length - Vector512<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
Vector512<float> value = (Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i)) - bias) * inverseScale;
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i)) = value;
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component1Base, i)) = value;
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component2Base, i)) = value;
}
}
if (Vector256.IsHardwareAccelerated && i <= length - Vector256<float>.Count)
{
Vector256<float> bias = Vector256.Create(parameters.LumaBias);
Vector256<float> inverseScale = Vector256.Create(1F / parameters.LumaScale);
int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
Vector256<float> value = (Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i)) - bias) * inverseScale;
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i)) = value;
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component1Base, i)) = value;
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component2Base, i)) = value;
}
}
if (Vector128.IsHardwareAccelerated && i <= length - Vector128<float>.Count)
{
Vector128<float> bias = Vector128.Create(parameters.LumaBias);
Vector128<float> inverseScale = Vector128.Create(1F / parameters.LumaScale);
int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
Vector128<float> value = (Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i)) - bias) * inverseScale;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i)) = value;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component1Base, i)) = value;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component2Base, i)) = value;
}
}
for (; i < length; i++)
{
float value = (Unsafe.Add(ref component0Base, i) - parameters.LumaBias) / parameters.LumaScale;
Unsafe.Add(ref component0Base, i) = value;
Unsafe.Add(ref component1Base, i) = value;
Unsafe.Add(ref component2Base, i) = value;
}
return;
}
float chromaBias = this.ChromaBias;
float chromaScale = this.ChromaScale;
// Descending widths preserve vector execution for the remainder left by a wider register. The range
// expansion is folded into each load so operators receive normalized H.273 components directly.
if (Vector512.IsHardwareAccelerated && i <= length - Vector512<float>.Count)
{
Vector512<float> lumaBias = Vector512.Create(parameters.LumaBias);
Vector512<float> inverseLumaScale = Vector512.Create(1F / parameters.LumaScale);
Vector512<float> chromaBiasVector = Vector512.Create(chromaBias);
Vector512<float> inverseChromaScale = Vector512.Create(1F / chromaScale);
int oneVectorFromEnd = length - Vector512<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
ref Vector512<float> c0 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i));
ref Vector512<float> c1 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component1Base, i));
ref Vector512<float> c2 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component2Base, i));
c0 = (c0 - lumaBias) * inverseLumaScale;
c1 = (c1 - chromaBiasVector) * inverseChromaScale;
c2 = (c2 - chromaBiasVector) * inverseChromaScale;
TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters);
}
}
if (Vector256.IsHardwareAccelerated && i <= length - Vector256<float>.Count)
{
Vector256<float> lumaBias = Vector256.Create(parameters.LumaBias);
Vector256<float> inverseLumaScale = Vector256.Create(1F / parameters.LumaScale);
Vector256<float> chromaBiasVector = Vector256.Create(chromaBias);
Vector256<float> inverseChromaScale = Vector256.Create(1F / chromaScale);
int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
ref Vector256<float> c0 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i));
ref Vector256<float> c1 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component1Base, i));
ref Vector256<float> c2 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component2Base, i));
c0 = (c0 - lumaBias) * inverseLumaScale;
c1 = (c1 - chromaBiasVector) * inverseChromaScale;
c2 = (c2 - chromaBiasVector) * inverseChromaScale;
TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters);
}
}
if (Vector128.IsHardwareAccelerated && i <= length - Vector128<float>.Count)
{
Vector128<float> lumaBias = Vector128.Create(parameters.LumaBias);
Vector128<float> inverseLumaScale = Vector128.Create(1F / parameters.LumaScale);
Vector128<float> chromaBiasVector = Vector128.Create(chromaBias);
Vector128<float> inverseChromaScale = Vector128.Create(1F / chromaScale);
int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
ref Vector128<float> c0 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i));
ref Vector128<float> c1 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component1Base, i));
ref Vector128<float> c2 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component2Base, i));
c0 = (c0 - lumaBias) * inverseLumaScale;
c1 = (c1 - chromaBiasVector) * inverseChromaScale;
c2 = (c2 - chromaBiasVector) * inverseChromaScale;
TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters);
}
}
// Scalar conversion is reserved for the zero-to-three samples left after the SIMD cascade.
for (; i < length; i++)
{
float c0 = (Unsafe.Add(ref component0Base, i) - parameters.LumaBias) / parameters.LumaScale;
float c1 = (Unsafe.Add(ref component1Base, i) - chromaBias) / chromaScale;
float c2 = (Unsafe.Add(ref component2Base, i) - chromaBias) / chromaScale;
TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters);
Unsafe.Add(ref component0Base, i) = c0;
Unsafe.Add(ref component1Base, i) = c1;
Unsafe.Add(ref component2Base, i) = c2;
}
}
/// <inheritdoc/>
public override void ConvertFromRgbInPlace(
Span<float> component0,
Span<float> component1,
Span<float> component2,
float maximumValue)
{
Av1ColorConversionParameters parameters = this.Parameters;
// The unpacker supplies three planar RGB rows. These same buffers become the destination component
// rows after each operator call, so encoding retains JPEG's planar contract without another allocation.
ref float component0Base = ref MemoryMarshal.GetReference(component0);
ref float component1Base = ref MemoryMarshal.GetReference(component1);
ref float component2Base = ref MemoryMarshal.GetReference(component2);
int length = component0.Length;
int i = 0;
// RGB normalization is part of the vector load, and each operator returns planar components through
// out parameters. This is the same input/output shape used by JPEG's encoder-side color operators.
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = length - Vector512<float>.Count;
if (i <= oneVectorFromEnd)
{
Vector512<float> inverseMaximum = Vector512.Create(1F / maximumValue);
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
Vector512<float> red = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i)) * inverseMaximum;
Vector512<float> green = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component1Base, i)) * inverseMaximum;
Vector512<float> blue = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component2Base, i)) * inverseMaximum;
ref Vector512<float> c0 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component0Base, i));
ref Vector512<float> c1 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component1Base, i));
ref Vector512<float> c2 = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref component2Base, i));
TOperator.ConvertFromRgb(red, green, blue, in parameters, out c0, out c1, out c2);
}
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = length - Vector256<float>.Count;
if (i <= oneVectorFromEnd)
{
Vector256<float> inverseMaximum = Vector256.Create(1F / maximumValue);
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
Vector256<float> red = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i)) * inverseMaximum;
Vector256<float> green = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component1Base, i)) * inverseMaximum;
Vector256<float> blue = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component2Base, i)) * inverseMaximum;
ref Vector256<float> c0 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component0Base, i));
ref Vector256<float> c1 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component1Base, i));
ref Vector256<float> c2 = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref component2Base, i));
TOperator.ConvertFromRgb(red, green, blue, in parameters, out c0, out c1, out c2);
}
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = length - Vector128<float>.Count;
if (i <= oneVectorFromEnd)
{
Vector128<float> inverseMaximum = Vector128.Create(1F / maximumValue);
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
Vector128<float> red = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i)) * inverseMaximum;
Vector128<float> green = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component1Base, i)) * inverseMaximum;
Vector128<float> blue = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component2Base, i)) * inverseMaximum;
ref Vector128<float> c0 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component0Base, i));
ref Vector128<float> c1 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component1Base, i));
ref Vector128<float> c2 = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref component2Base, i));
TOperator.ConvertFromRgb(red, green, blue, in parameters, out c0, out c1, out c2);
}
}
}
float inverseMaximumScalar = 1F / maximumValue;
// The shared offset leaves at most three samples for the scalar fallback on SIMD-capable systems.
for (; i < length; i++)
{
float red = Unsafe.Add(ref component0Base, i) * inverseMaximumScalar;
float green = Unsafe.Add(ref component1Base, i) * inverseMaximumScalar;
float blue = Unsafe.Add(ref component2Base, i) * inverseMaximumScalar;
TOperator.ConvertFromRgb(red, green, blue, in parameters, out float c0, out float c1, out float c2);
Unsafe.Add(ref component0Base, i) = c0;
Unsafe.Add(ref component1Base, i) = c1;
Unsafe.Add(ref component2Base, i) = c2;
}
}
}
}

143
src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.Smpte2085Operator.cs

@ -0,0 +1,143 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
internal abstract partial class Av1ColorConverterBase
{
/// <summary>
/// Implements SMPTE ST 2085 YDzDx conversion for scalar and SIMD lanes.
/// </summary>
internal readonly struct Av1Smpte2085ColorOperator : IAv1ColorOperator
{
/// <summary>
/// The SMPTE ST 2085 blue primary normalization factor.
/// </summary>
public const float BlueNormalization = 0.986566F;
/// <summary>
/// The SMPTE ST 2085 green contribution to the red primary.
/// </summary>
public const float RedGreenContribution = 0.991902F;
/// <inheritdoc/>
public static bool ChromaUsesLumaRange => false;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref float y, ref float dz, ref float dx, in Av1ColorConversionParameters parameters)
{
// The encoded YDzDx planes carry green directly. The two difference planes restore blue and red.
float g = y;
float b = ((2F * dz) + y) / BlueNormalization;
float r = (2F * dx) + (RedGreenContribution * y);
y = r;
dz = g;
dx = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector128<float> y, ref Vector128<float> dz, ref Vector128<float> dx, in Av1ColorConversionParameters parameters)
{
Vector128<float> g = y;
Vector128<float> b = Vector128.MultiplyAddEstimate(Vector128.Create(2F), dz, y) / Vector128.Create(BlueNormalization);
Vector128<float> r = Vector128.MultiplyAddEstimate(Vector128.Create(2F), dx, Vector128.Create(RedGreenContribution) * y);
y = r;
dz = g;
dx = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector256<float> y, ref Vector256<float> dz, ref Vector256<float> dx, in Av1ColorConversionParameters parameters)
{
Vector256<float> g = y;
Vector256<float> b = Vector256.MultiplyAddEstimate(Vector256.Create(2F), dz, y) / Vector256.Create(BlueNormalization);
Vector256<float> r = Vector256.MultiplyAddEstimate(Vector256.Create(2F), dx, Vector256.Create(RedGreenContribution) * y);
y = r;
dz = g;
dx = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector512<float> y, ref Vector512<float> dz, ref Vector512<float> dx, in Av1ColorConversionParameters parameters)
{
Vector512<float> g = y;
Vector512<float> b = Vector512.MultiplyAddEstimate(Vector512.Create(2F), dz, y) / Vector512.Create(BlueNormalization);
Vector512<float> r = Vector512.MultiplyAddEstimate(Vector512.Create(2F), dx, Vector512.Create(RedGreenContribution) * y);
y = r;
dz = g;
dx = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
float r,
float g,
float b,
in Av1ColorConversionParameters parameters,
out float y,
out float dz,
out float dx)
{
// Y is the green primary; Dz and Dx are half-scaled blue and red differences.
y = g;
dz = ((BlueNormalization * b) - y) * 0.5F;
dx = (r - (RedGreenContribution * y)) * 0.5F;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> r,
Vector128<float> g,
Vector128<float> b,
in Av1ColorConversionParameters parameters,
out Vector128<float> y,
out Vector128<float> dz,
out Vector128<float> dx)
{
y = g;
dz = Vector128.Create(0.5F) * Vector128.MultiplyAddEstimate(Vector128.Create(BlueNormalization), b, -y);
dx = Vector128.Create(0.5F) * Vector128.MultiplyAddEstimate(Vector128.Create(-RedGreenContribution), y, r);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> r,
Vector256<float> g,
Vector256<float> b,
in Av1ColorConversionParameters parameters,
out Vector256<float> y,
out Vector256<float> dz,
out Vector256<float> dx)
{
y = g;
dz = Vector256.Create(0.5F) * Vector256.MultiplyAddEstimate(Vector256.Create(BlueNormalization), b, -y);
dx = Vector256.Create(0.5F) * Vector256.MultiplyAddEstimate(Vector256.Create(-RedGreenContribution), y, r);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> r,
Vector512<float> g,
Vector512<float> b,
in Av1ColorConversionParameters parameters,
out Vector512<float> y,
out Vector512<float> dz,
out Vector512<float> dx)
{
y = g;
dz = Vector512.Create(0.5F) * Vector512.MultiplyAddEstimate(Vector512.Create(BlueNormalization), b, -y);
dx = Vector512.Create(0.5F) * Vector512.MultiplyAddEstimate(Vector512.Create(-RedGreenContribution), y, r);
}
}
}

141
src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.YCgCoOperator.cs

@ -0,0 +1,141 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
internal abstract partial class Av1ColorConverterBase
{
/// <summary>
/// Implements YCgCo conversion for scalar and SIMD lanes.
/// </summary>
internal readonly struct Av1YCgCoColorOperator : IAv1ColorOperator
{
/// <inheritdoc/>
public static bool ChromaUsesLumaRange => false;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref float y, ref float cg, ref float co, in Av1ColorConversionParameters parameters)
{
// Reusing Y - Cg for both outer primaries keeps the inverse transform to four additions.
float temporary = y - cg;
float r = temporary + co;
float g = y + cg;
float b = temporary - co;
y = r;
cg = g;
co = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector128<float> y, ref Vector128<float> cg, ref Vector128<float> co, in Av1ColorConversionParameters parameters)
{
Vector128<float> temporary = y - cg;
Vector128<float> r = temporary + co;
Vector128<float> g = y + cg;
Vector128<float> b = temporary - co;
y = r;
cg = g;
co = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector256<float> y, ref Vector256<float> cg, ref Vector256<float> co, in Av1ColorConversionParameters parameters)
{
Vector256<float> temporary = y - cg;
Vector256<float> r = temporary + co;
Vector256<float> g = y + cg;
Vector256<float> b = temporary - co;
y = r;
cg = g;
co = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref Vector512<float> y, ref Vector512<float> cg, ref Vector512<float> co, in Av1ColorConversionParameters parameters)
{
Vector512<float> temporary = y - cg;
Vector512<float> r = temporary + co;
Vector512<float> g = y + cg;
Vector512<float> b = temporary - co;
y = r;
cg = g;
co = b;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
float r,
float g,
float b,
in Av1ColorConversionParameters parameters,
out float y,
out float cg,
out float co)
{
// R + B is shared by Y and Cg, while Co is the half-scaled red/blue difference.
float sum = r + b;
y = (0.5F * g) + (0.25F * sum);
cg = (0.5F * g) - (0.25F * sum);
co = 0.5F * (r - b);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> r,
Vector128<float> g,
Vector128<float> b,
in Av1ColorConversionParameters parameters,
out Vector128<float> y,
out Vector128<float> cg,
out Vector128<float> co)
{
Vector128<float> sum = r + b;
y = Vector128.MultiplyAddEstimate(Vector128.Create(0.5F), g, Vector128.Create(0.25F) * sum);
cg = Vector128.MultiplyAddEstimate(Vector128.Create(0.5F), g, Vector128.Create(-0.25F) * sum);
co = Vector128.Create(0.5F) * (r - b);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> r,
Vector256<float> g,
Vector256<float> b,
in Av1ColorConversionParameters parameters,
out Vector256<float> y,
out Vector256<float> cg,
out Vector256<float> co)
{
Vector256<float> sum = r + b;
y = Vector256.MultiplyAddEstimate(Vector256.Create(0.5F), g, Vector256.Create(0.25F) * sum);
cg = Vector256.MultiplyAddEstimate(Vector256.Create(0.5F), g, Vector256.Create(-0.25F) * sum);
co = Vector256.Create(0.5F) * (r - b);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> r,
Vector512<float> g,
Vector512<float> b,
in Av1ColorConversionParameters parameters,
out Vector512<float> y,
out Vector512<float> cg,
out Vector512<float> co)
{
Vector512<float> sum = r + b;
y = Vector512.MultiplyAddEstimate(Vector512.Create(0.5F), g, Vector512.Create(0.25F) * sum);
cg = Vector512.MultiplyAddEstimate(Vector512.Create(0.5F), g, Vector512.Create(-0.25F) * sum);
co = Vector512.Create(0.5F) * (r - b);
}
}
}

126
src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.cs

@ -0,0 +1,126 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <summary>
/// Identifies the H.273 matrix operation used between encoded planes and RGB components.
/// </summary>
internal enum Av1ColorConversionMode
{
/// <summary>
/// A coefficient-based YCbCr matrix conversion.
/// </summary>
Coefficients,
/// <summary>
/// Direct G, B, and R component mapping from the Y, U, and V planes.
/// </summary>
Identity,
/// <summary>
/// The reversible-style YCgCo color transform.
/// </summary>
YCgCo,
/// <summary>
/// The SMPTE ST 2085 YDzDx color transform.
/// </summary>
Smpte2085,
/// <summary>
/// A constant-luminance transform using the signaled transfer characteristics.
/// </summary>
ConstantLuminance,
/// <summary>
/// The BT.2100 ICtCp color transform.
/// </summary>
ICtCp,
}
/// <summary>
/// Converts normalized component planes between an AV1 color model and RGB.
/// </summary>
internal abstract partial class Av1ColorConverterBase
{
/// <summary>
/// Initializes a new instance of the <see cref="Av1ColorConverterBase"/> class.
/// </summary>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="isMonochrome">Whether the frame contains only luma samples.</param>
protected Av1ColorConverterBase(in Av1ColorConversionParameters parameters, bool isMonochrome)
{
this.Parameters = parameters;
this.IsMonochrome = isMonochrome;
}
/// <summary>
/// Gets the resolved H.273 conversion parameters.
/// </summary>
protected Av1ColorConversionParameters Parameters { get; }
/// <summary>
/// Gets a value indicating whether the frame contains only luma samples.
/// </summary>
protected bool IsMonochrome { get; }
/// <summary>
/// Gets the scale used to encode luma components.
/// </summary>
public float LumaScale => this.Parameters.LumaScale;
/// <summary>
/// Gets the bias used to encode luma components.
/// </summary>
public float LumaBias => this.Parameters.LumaBias;
/// <summary>
/// Gets the scale used to encode chroma components.
/// </summary>
public abstract float ChromaScale { get; }
/// <summary>
/// Gets the bias used to encode chroma components.
/// </summary>
public abstract float ChromaBias { get; }
/// <summary>
/// Converts normalized AV1 components to normalized RGB in place.
/// </summary>
/// <param name="component0">The luma or first color component, replaced by red.</param>
/// <param name="component1">The first chroma or second color component, replaced by green.</param>
/// <param name="component2">The second chroma or third color component, replaced by blue.</param>
public abstract void ConvertToRgbInPlace(Span<float> component0, Span<float> component1, Span<float> component2);
/// <summary>
/// Converts normalized RGB components to normalized AV1 components in place.
/// </summary>
/// <param name="component0">The red component, replaced by luma or the first color component.</param>
/// <param name="component1">The green component, replaced by the first chroma or second color component.</param>
/// <param name="component2">The blue component, replaced by the second chroma or third color component.</param>
/// <param name="maximumValue">The largest value in the RGB component planes.</param>
public abstract void ConvertFromRgbInPlace(
Span<float> component0,
Span<float> component1,
Span<float> component2,
float maximumValue);
/// <summary>
/// Creates the converter selected by the resolved H.273 matrix and transfer characteristics.
/// </summary>
/// <param name="mode">The resolved matrix conversion mode.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="isMonochrome">Whether the frame contains only luma samples.</param>
/// <returns>The selected converter.</returns>
public static Av1ColorConverterBase Create(Av1ColorConversionMode mode, in Av1ColorConversionParameters parameters, bool isMonochrome)
=> mode switch
{
Av1ColorConversionMode.Identity => new Av1ColorConverter<Av1IdentityColorOperator>(in parameters, isMonochrome),
Av1ColorConversionMode.YCgCo => new Av1ColorConverter<Av1YCgCoColorOperator>(in parameters, isMonochrome),
Av1ColorConversionMode.Smpte2085 => new Av1ColorConverter<Av1Smpte2085ColorOperator>(in parameters, isMonochrome),
Av1ColorConversionMode.ConstantLuminance => new Av1ColorConverter<Av1ConstantLuminanceColorOperator>(in parameters, isMonochrome),
Av1ColorConversionMode.ICtCp => new Av1ColorConverter<Av1ICtCpColorOperator>(in parameters, isMonochrome),
_ => new Av1ColorConverter<Av1CoefficientColorOperator>(in parameters, isMonochrome),
};
}

40
src/ImageSharp/Formats/Heif/Av1/Av1TransferFunctions.Simd.cs

@ -641,22 +641,6 @@ internal static class Av1TransferVectorOperators
/// <param name="value">The input vector.</param>
/// <returns>The lane-wise square root.</returns>
public static abstract TVector Sqrt(TVector value);
/// <summary>
/// Converts nonlinear signal lanes to their H.273 linear-domain values.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The nonlinear signal values.</param>
/// <returns>The corresponding linear-domain values.</returns>
public static abstract TVector ToLinear(ObuTransferCharacteristics transferCharacteristics, TVector value);
/// <summary>
/// Converts linear signal lanes to their H.273 nonlinear-domain values.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="value">The linear signal values.</param>
/// <returns>The corresponding nonlinear-domain values.</returns>
public static abstract TVector ToGamma(ObuTransferCharacteristics transferCharacteristics, TVector value);
}
/// <summary>
@ -710,14 +694,6 @@ internal static class Av1TransferVectorOperators
/// <inheritdoc/>
public static Vector128<float> Sqrt(Vector128<float> value) => Vector128.Sqrt(value);
/// <inheritdoc/>
public static Vector128<float> ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector128<float> value)
=> Av1TransferFunctions.ToLinear(transferCharacteristics, value);
/// <inheritdoc/>
public static Vector128<float> ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector128<float> value)
=> Av1TransferFunctions.ToGamma(transferCharacteristics, value);
}
/// <summary>
@ -771,14 +747,6 @@ internal static class Av1TransferVectorOperators
/// <inheritdoc/>
public static Vector256<float> Sqrt(Vector256<float> value) => Vector256.Sqrt(value);
/// <inheritdoc/>
public static Vector256<float> ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector256<float> value)
=> Av1TransferFunctions.ToLinear(transferCharacteristics, value);
/// <inheritdoc/>
public static Vector256<float> ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector256<float> value)
=> Av1TransferFunctions.ToGamma(transferCharacteristics, value);
}
/// <summary>
@ -832,13 +800,5 @@ internal static class Av1TransferVectorOperators
/// <inheritdoc/>
public static Vector512<float> Sqrt(Vector512<float> value) => Vector512.Sqrt(value);
/// <inheritdoc/>
public static Vector512<float> ToLinear(ObuTransferCharacteristics transferCharacteristics, Vector512<float> value)
=> Av1TransferFunctions.ToLinear(transferCharacteristics, value);
/// <inheritdoc/>
public static Vector512<float> ToGamma(ObuTransferCharacteristics transferCharacteristics, Vector512<float> value)
=> Av1TransferFunctions.ToGamma(transferCharacteristics, value);
}
}

623
src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Operators.cs

@ -1,623 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using static SixLabors.ImageSharp.Formats.Heif.Av1.Av1TransferVectorOperators;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <content>
/// Provides the stateless scalar and SIMD operators used by AV1 YUV-to-RGB row conversion.
/// </content>
internal static partial class Av1YuvConverter
{
/// <summary>
/// Defines one H.273 YUV-to-RGB operation for scalar and SIMD traversal.
/// </summary>
private interface IYuvToRgbOperator
{
/// <summary>
/// Gets a value indicating whether the operator consumes chroma components.
/// </summary>
public static abstract bool UsesChroma { get; }
/// <summary>
/// Converts one normalized YUV sample to RGB.
/// </summary>
/// <param name="y">The normalized luma, replaced by red.</param>
/// <param name="cb">The normalized blue-difference component, replaced by green.</param>
/// <param name="cr">The normalized red-difference component, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters);
/// <summary>
/// Converts four normalized YUV samples to RGB.
/// </summary>
/// <param name="y">The normalized luma lanes, replaced by red.</param>
/// <param name="cb">The normalized blue-difference lanes, replaced by green.</param>
/// <param name="cr">The normalized red-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters);
/// <summary>
/// Converts eight normalized YUV samples to RGB.
/// </summary>
/// <param name="y">The normalized luma lanes, replaced by red.</param>
/// <param name="cb">The normalized blue-difference lanes, replaced by green.</param>
/// <param name="cr">The normalized red-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters);
/// <summary>
/// Converts sixteen normalized YUV samples to RGB.
/// </summary>
/// <param name="y">The normalized luma lanes, replaced by red.</param>
/// <param name="cb">The normalized blue-difference lanes, replaced by green.</param>
/// <param name="cr">The normalized red-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static abstract void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters);
}
/// <summary>
/// Stores the resolved H.273 values shared by every scalar and SIMD lane.
/// </summary>
private readonly struct YuvToRgbParameters
{
/// <summary>
/// Initializes a new instance of the <see cref="YuvToRgbParameters"/> struct.
/// </summary>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kg">The green luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="constantLuminanceScales">The constant-luminance chroma scales.</param>
/// <param name="lumaBias">The encoded luma bias.</param>
/// <param name="lumaScale">The encoded luma range.</param>
/// <param name="chromaBias">The encoded chroma midpoint.</param>
/// <param name="chromaScale">The encoded chroma range.</param>
public YuvToRgbParameters(
float kr,
float kg,
float kb,
ObuTransferCharacteristics transferCharacteristics,
in ConstantLuminanceScales constantLuminanceScales,
float lumaBias,
float lumaScale,
float chromaBias,
float chromaScale)
{
this.Kr = kr;
this.Kg = kg;
this.Kb = kb;
this.TransferCharacteristics = transferCharacteristics;
this.ConstantLuminanceScales = constantLuminanceScales;
this.LumaBias = lumaBias;
this.LumaScale = lumaScale;
this.ChromaBias = chromaBias;
this.ChromaScale = chromaScale;
}
/// <summary>
/// Gets the red luma coefficient.
/// </summary>
public float Kr { get; }
/// <summary>
/// Gets the green luma coefficient.
/// </summary>
public float Kg { get; }
/// <summary>
/// Gets the blue luma coefficient.
/// </summary>
public float Kb { get; }
/// <summary>
/// Gets the signaled transfer characteristics.
/// </summary>
public ObuTransferCharacteristics TransferCharacteristics { get; }
/// <summary>
/// Gets the constant-luminance chroma scales.
/// </summary>
public ConstantLuminanceScales ConstantLuminanceScales { get; }
/// <summary>
/// Gets the encoded luma bias.
/// </summary>
public float LumaBias { get; }
/// <summary>
/// Gets the encoded luma range.
/// </summary>
public float LumaScale { get; }
/// <summary>
/// Gets the encoded chroma midpoint.
/// </summary>
public float ChromaBias { get; }
/// <summary>
/// Gets the encoded chroma range.
/// </summary>
public float ChromaScale { get; }
}
/// <summary>
/// Replicates luma into all three RGB components for monochrome input.
/// </summary>
private readonly struct MonochromeOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => false;
/// <inheritdoc/>
public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters)
{
cb = y;
cr = y;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
{
cb = y;
cr = y;
}
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
{
cb = y;
cr = y;
}
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
{
cb = y;
cr = y;
}
}
/// <summary>
/// Converts coefficient-based YCbCr signals to RGB.
/// </summary>
private readonly struct CoefficientsOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters)
{
float r = y + (2F * (1F - parameters.Kr) * cr);
float g = y - (2F * ((parameters.Kr * (1F - parameters.Kr) * cr) + (parameters.Kb * (1F - parameters.Kb) * cb)) / parameters.Kg);
float b = y + (2F * (1F - parameters.Kb) * cb);
y = r;
cb = g;
cr = b;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertCoefficients<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertCoefficients<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertCoefficients<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr, in parameters);
}
/// <summary>
/// Maps identity-coded G, B, and R planes to RGB.
/// </summary>
private readonly struct IdentityOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters)
{
float r = (((cr * parameters.ChromaScale) + parameters.ChromaBias) - parameters.LumaBias) / parameters.LumaScale;
float b = (((cb * parameters.ChromaScale) + parameters.ChromaBias) - parameters.LumaBias) / parameters.LumaScale;
cb = y;
y = r;
cr = b;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertIdentity<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertIdentity<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertIdentity<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr, in parameters);
}
/// <summary>
/// Converts YCgCo signals to RGB.
/// </summary>
private readonly struct YCgCoOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters)
{
float temporary = y - cb;
float r = temporary + cr;
float g = y + cb;
float b = temporary - cr;
y = r;
cb = g;
cr = b;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertYCgCo<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertYCgCo<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertYCgCo<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr);
}
/// <summary>
/// Converts SMPTE ST 2085 YDzDx signals to RGB.
/// </summary>
private readonly struct Smpte2085Operator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters)
{
float g = y;
float b = ((2F * cb) + y) / 0.986566F;
float r = (2F * cr) + (0.991902F * y);
y = r;
cb = g;
cr = b;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertSmpte2085<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertSmpte2085<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertSmpte2085<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr);
}
/// <summary>
/// Converts H.273 constant-luminance signals to RGB.
/// </summary>
private readonly struct ConstantLuminanceOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters)
{
ConstantLuminanceScales scales = parameters.ConstantLuminanceScales;
float nonlinearBlue = y + (2F * (cb <= 0F ? scales.NegativeBlue : scales.PositiveBlue) * cb);
float nonlinearRed = y + (2F * (cr <= 0F ? scales.NegativeRed : scales.PositiveRed) * cr);
float linearY = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, y);
float linearBlue = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue);
float linearRed = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed);
float linearGreen = (linearY - (parameters.Kr * linearRed) - (parameters.Kb * linearBlue)) / parameters.Kg;
y = nonlinearRed;
cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
cr = nonlinearBlue;
}
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertConstantLuminance<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertConstantLuminance<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr, in parameters);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertConstantLuminance<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr, in parameters);
}
/// <summary>
/// Converts the PQ-family ICtCp matrix to RGB.
/// </summary>
private readonly struct ICtCpOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCpScalar(ref y, ref cb, ref cr, in parameters, false);
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr, in parameters, false);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr, in parameters, false);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr, in parameters, false);
}
/// <summary>
/// Converts the HLG-specific ICtCp matrix to RGB.
/// </summary>
private readonly struct ICtCpHlgOperator : IYuvToRgbOperator
{
/// <inheritdoc/>
public static bool UsesChroma => true;
/// <inheritdoc/>
public static void Convert(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCpScalar(ref y, ref cb, ref cr, in parameters, true);
/// <inheritdoc/>
public static void Convert(ref Vector128<float> y, ref Vector128<float> cb, ref Vector128<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector128<float>, Vector128Operator>(ref y, ref cb, ref cr, in parameters, true);
/// <inheritdoc/>
public static void Convert(ref Vector256<float> y, ref Vector256<float> cb, ref Vector256<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector256<float>, Vector256Operator>(ref y, ref cb, ref cr, in parameters, true);
/// <inheritdoc/>
public static void Convert(ref Vector512<float> y, ref Vector512<float> cb, ref Vector512<float> cr, in YuvToRgbParameters parameters)
=> YuvToRgbMath.ConvertICtCp<Vector512<float>, Vector512Operator>(ref y, ref cb, ref cr, in parameters, true);
}
/// <summary>
/// Contains the shared vector-width-independent arithmetic used by the color operators.
/// </summary>
private static class YuvToRgbMath
{
/// <summary>
/// Converts coefficient-based YCbCr SIMD lanes to RGB.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The luma lanes, replaced by red.</param>
/// <param name="cb">The blue-difference lanes, replaced by green.</param>
/// <param name="cr">The red-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static void ConvertCoefficients<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector r = TOperator.MultiplyAddEstimate(TOperator.Create(2F * (1F - parameters.Kr)), cr, y);
TVector greenDifference = TOperator.MultiplyAddEstimate(
TOperator.Create(parameters.Kr * (1F - parameters.Kr)),
cr,
TOperator.Multiply(TOperator.Create(parameters.Kb * (1F - parameters.Kb)), cb));
TVector g = TOperator.Subtract(y, TOperator.Multiply(TOperator.Create(2F / parameters.Kg), greenDifference));
TVector b = TOperator.MultiplyAddEstimate(TOperator.Create(2F * (1F - parameters.Kb)), cb, y);
y = r;
cb = g;
cr = b;
}
/// <summary>
/// Maps identity-coded SIMD lanes to RGB.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The green lanes, replaced by red.</param>
/// <param name="cb">The normalized blue plane, replaced by green.</param>
/// <param name="cr">The normalized red plane, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static void ConvertIdentity<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector lumaBias = TOperator.Create(parameters.LumaBias);
TVector inverseLumaScale = TOperator.Create(1F / parameters.LumaScale);
TVector chromaScale = TOperator.Create(parameters.ChromaScale);
TVector chromaBias = TOperator.Create(parameters.ChromaBias);
TVector r = TOperator.Multiply(TOperator.Subtract(TOperator.MultiplyAddEstimate(cr, chromaScale, chromaBias), lumaBias), inverseLumaScale);
TVector b = TOperator.Multiply(TOperator.Subtract(TOperator.MultiplyAddEstimate(cb, chromaScale, chromaBias), lumaBias), inverseLumaScale);
cb = y;
y = r;
cr = b;
}
/// <summary>
/// Converts YCgCo SIMD lanes to RGB.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The luma lanes, replaced by red.</param>
/// <param name="cb">The green-difference lanes, replaced by green.</param>
/// <param name="cr">The orange-difference lanes, replaced by blue.</param>
public static void ConvertYCgCo<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector temporary = TOperator.Subtract(y, cb);
TVector r = TOperator.Add(temporary, cr);
TVector g = TOperator.Add(y, cb);
TVector b = TOperator.Subtract(temporary, cr);
y = r;
cb = g;
cr = b;
}
/// <summary>
/// Converts SMPTE ST 2085 SIMD lanes to RGB.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The luma lanes, replaced by red.</param>
/// <param name="cb">The blue-difference lanes, replaced by green.</param>
/// <param name="cr">The red-difference lanes, replaced by blue.</param>
public static void ConvertSmpte2085<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector g = y;
TVector b = TOperator.Multiply(TOperator.MultiplyAddEstimate(TOperator.Create(2F), cb, y), TOperator.Create(1F / 0.986566F));
TVector r = TOperator.MultiplyAddEstimate(TOperator.Create(2F), cr, TOperator.Multiply(TOperator.Create(0.991902F), y));
y = r;
cb = g;
cr = b;
}
/// <summary>
/// Converts constant-luminance SIMD lanes to RGB.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The nonlinear luma lanes, replaced by red.</param>
/// <param name="cb">The blue-difference lanes, replaced by green.</param>
/// <param name="cr">The red-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public static void ConvertConstantLuminance<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
TVector zero = TOperator.Create(0F);
ConstantLuminanceScales scales = parameters.ConstantLuminanceScales;
TVector blueScale = TOperator.ConditionalSelect(
TOperator.LessThanOrEqual(cb, zero),
TOperator.Create(scales.NegativeBlue),
TOperator.Create(scales.PositiveBlue));
TVector redScale = TOperator.ConditionalSelect(
TOperator.LessThanOrEqual(cr, zero),
TOperator.Create(scales.NegativeRed),
TOperator.Create(scales.PositiveRed));
TVector nonlinearBlue = TOperator.MultiplyAddEstimate(TOperator.Multiply(TOperator.Create(2F), blueScale), cb, y);
TVector nonlinearRed = TOperator.MultiplyAddEstimate(TOperator.Multiply(TOperator.Create(2F), redScale), cr, y);
TVector linearY = TOperator.ToLinear(parameters.TransferCharacteristics, y);
TVector linearBlue = TOperator.ToLinear(parameters.TransferCharacteristics, nonlinearBlue);
TVector linearRed = TOperator.ToLinear(parameters.TransferCharacteristics, nonlinearRed);
TVector redAndBlue = TOperator.MultiplyAddEstimate(
TOperator.Create(parameters.Kr),
linearRed,
TOperator.Multiply(TOperator.Create(parameters.Kb), linearBlue));
TVector linearGreen = TOperator.Divide(TOperator.Subtract(linearY, redAndBlue), TOperator.Create(parameters.Kg));
y = nonlinearRed;
cb = TOperator.ToGamma(parameters.TransferCharacteristics, linearGreen);
cr = nonlinearBlue;
}
/// <summary>
/// Converts one ICtCp sample to RGB using the selected inverse matrix.
/// </summary>
/// <param name="y">The intensity, replaced by red.</param>
/// <param name="cb">The tritan-difference component, replaced by green.</param>
/// <param name="cr">The protan-difference component, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="isHlg">Whether to use the HLG-specific inverse ICtCp matrix.</param>
public static void ConvertICtCpScalar(ref float y, ref float cb, ref float cr, in YuvToRgbParameters parameters, bool isHlg)
{
float nonlinearL = isHlg
? y + (0.015718580108730413F * cb) + (0.2095810681164055F * cr)
: y + (0.008609037037932756F * cb) + (0.11102962500302596F * cr);
float nonlinearM = isHlg
? y - (0.015718580108730413F * cb) - (0.2095810681164055F * cr)
: y - (0.008609037037932756F * cb) - (0.11102962500302596F * cr);
float nonlinearS = isHlg
? y + (1.0212710798422342F * cb) - (0.6052744909924315F * cr)
: y + (0.5600313357106791F * cb) - (0.32062717498731885F * cr);
float linearL = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL);
float linearM = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM);
float linearS = Av1TransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS);
float linearRed = (3.4366066943330784F * linearL) - (2.50645211865627F * linearM) + (0.06984542432319148F * linearS);
float linearGreen = (-0.7913295555989287F * linearL) + (1.9836004517922907F * linearM) - (0.192270896193362F * linearS);
float linearBlue = (-0.025949899690592672F * linearL) - (0.09891371471172644F * linearM) + (1.1248636144023192F * linearS);
y = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed);
cb = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
cr = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
}
/// <summary>
/// Converts ICtCp SIMD lanes to RGB using the selected inverse matrix.
/// </summary>
/// <typeparam name="TVector">The SIMD vector type.</typeparam>
/// <typeparam name="TOperator">The operations for the SIMD vector type.</typeparam>
/// <param name="y">The intensity lanes, replaced by red.</param>
/// <param name="cb">The tritan-difference lanes, replaced by green.</param>
/// <param name="cr">The protan-difference lanes, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
/// <param name="isHlg">Whether to use the HLG-specific inverse ICtCp matrix.</param>
public static void ConvertICtCp<TVector, TOperator>(ref TVector y, ref TVector cb, ref TVector cr, in YuvToRgbParameters parameters, bool isHlg)
where TVector : struct
where TOperator : struct, ITransferVectorOperator<TVector>
{
float cbToL = isHlg ? 0.015718580108730413F : 0.008609037037932756F;
float crToL = isHlg ? 0.2095810681164055F : 0.11102962500302596F;
float cbToS = isHlg ? 1.0212710798422342F : 0.5600313357106791F;
float crToS = isHlg ? -0.6052744909924315F : -0.32062717498731885F;
TVector cbContribution = TOperator.Multiply(TOperator.Create(cbToL), cb);
TVector crContribution = TOperator.Multiply(TOperator.Create(crToL), cr);
TVector nonlinearL = TOperator.Add(TOperator.Add(y, cbContribution), crContribution);
TVector nonlinearM = TOperator.Subtract(TOperator.Subtract(y, cbContribution), crContribution);
TVector nonlinearS = TOperator.MultiplyAddEstimate(TOperator.Create(crToS), cr, TOperator.MultiplyAddEstimate(TOperator.Create(cbToS), cb, y));
TVector linearL = TOperator.ToLinear(parameters.TransferCharacteristics, nonlinearL);
TVector linearM = TOperator.ToLinear(parameters.TransferCharacteristics, nonlinearM);
TVector linearS = TOperator.ToLinear(parameters.TransferCharacteristics, nonlinearS);
TVector linearRed = TOperator.MultiplyAddEstimate(
TOperator.Create(0.06984542432319148F),
linearS,
TOperator.MultiplyAddEstimate(
TOperator.Create(-2.50645211865627F),
linearM,
TOperator.Multiply(TOperator.Create(3.4366066943330784F), linearL)));
TVector linearGreen = TOperator.MultiplyAddEstimate(
TOperator.Create(-0.192270896193362F),
linearS,
TOperator.MultiplyAddEstimate(
TOperator.Create(1.9836004517922907F),
linearM,
TOperator.Multiply(TOperator.Create(-0.7913295555989287F), linearL)));
TVector linearBlue = TOperator.MultiplyAddEstimate(
TOperator.Create(1.1248636144023192F),
linearS,
TOperator.MultiplyAddEstimate(
TOperator.Create(-0.09891371471172644F),
linearM,
TOperator.Multiply(TOperator.Create(-0.025949899690592672F), linearL)));
y = TOperator.ToGamma(parameters.TransferCharacteristics, linearRed);
cb = TOperator.ToGamma(parameters.TransferCharacteristics, linearGreen);
cr = TOperator.ToGamma(parameters.TransferCharacteristics, linearBlue);
}
}
}

426
src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs

@ -0,0 +1,426 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <content>
/// Provides the pooled row converters used by AV1 color conversion.
/// </content>
internal static partial class Av1YuvConverter
{
/// <summary>
/// Converts one reconstructed AV1 row using frame-scoped pooled component, chroma, and packed-pixel storage.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <typeparam name="TSample">The reconstructed sample type.</typeparam>
/// <typeparam name="TLoader">The SIMD widening operations for the sample type.</typeparam>
private readonly struct YuvToRgbRowConverter<TPixel, TSample, TLoader>
where TPixel : unmanaged, IPixel<TPixel>
where TSample : unmanaged
where TLoader : struct, ISampleLoader<TSample>
{
/// <summary>
/// The configuration used by bulk pixel conversion.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The reconstructed AV1 frame containing the source planes.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// The destination image frame.
/// </summary>
private readonly ImageFrame<TPixel> image;
/// <summary>
/// The full-resolution luma plane.
/// </summary>
private readonly Buffer2DRegion<byte> yPlane;
/// <summary>
/// The blue-difference plane when the frame contains chroma.
/// </summary>
private readonly Buffer2DRegion<byte> uPlane;
/// <summary>
/// The red-difference plane when the frame contains chroma.
/// </summary>
private readonly Buffer2DRegion<byte> vPlane;
/// <summary>
/// The frame-scoped color-model converter.
/// </summary>
private readonly Av1ColorConverterBase colorConverter;
/// <summary>
/// The signaled chroma sample position used for reconstruction.
/// </summary>
private readonly ObuChromoSamplePosition chromaSamplePosition;
/// <summary>
/// Whether the source contains only a luma plane.
/// </summary>
private readonly bool isMonochrome;
/// <summary>
/// The horizontal chroma subsampling shift.
/// </summary>
private readonly int subX;
/// <summary>
/// The vertical chroma subsampling shift.
/// </summary>
private readonly int subY;
/// <summary>
/// Initializes a new instance of the <see cref="YuvToRgbRowConverter{TPixel, TSample, TLoader}"/> struct.
/// </summary>
/// <param name="configuration">The configuration used for packed-pixel conversion.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame.</param>
/// <param name="image">The destination image frame.</param>
/// <param name="colorConverter">The selected H.273 color converter.</param>
public YuvToRgbRowConverter(
Configuration configuration,
Av1FrameBuffer<byte> frameBuffer,
ImageFrame<TPixel> image,
Av1ColorConverterBase colorConverter)
{
this.configuration = configuration;
this.frameBuffer = frameBuffer;
this.image = image;
this.colorConverter = colorConverter;
this.isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
this.subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0;
this.subY = frameBuffer.ColorConfig.SubSamplingY ? 1 : 0;
this.chromaSamplePosition = frameBuffer.ColorConfig.ChromaSamplePosition;
this.yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0);
this.uPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, this.subX, this.subY);
this.vPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, this.subX, this.subY);
}
/// <summary>
/// Gets the number of float elements required by the reusable conversion buffer.
/// </summary>
public int BufferLength
{
get
{
// Three float component rows are converted in place. Color input adds two reusable chroma scratch
// rows, and the final one or two float-sized slots per pixel back an Rgba32 or Rgba64 staging row.
int rowCount = this.isMonochrome ? 3 : 5;
int packedRowCount = typeof(TSample) == typeof(byte) ? 1 : 2;
return this.image.Width * (rowCount + packedRowCount);
}
}
/// <summary>
/// Converts one reconstructed AV1 row to packed pixels.
/// </summary>
/// <param name="y">The row index.</param>
/// <param name="span">The reusable conversion buffer.</param>
public void Convert(int y, Span<float> span)
{
int width = this.image.Width;
Span<float> red = span[..width];
Span<float> green = span.Slice(width, width);
Span<float> blue = span.Slice(width * 2, width);
ReadOnlySpan<TSample> ySource;
if (typeof(TSample) == typeof(byte))
{
ySource = MemoryMarshal.Cast<byte, TSample>(this.yPlane.DangerousGetRowSpan(y));
}
else
{
ySource = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0));
}
ConvertSamplesToFloat<TSample, TLoader>(ySource, red);
int packedOffset = width * 3;
if (!this.isMonochrome)
{
GetChromaCoordinates(
y,
this.subY,
this.subY != 0 && this.chromaSamplePosition != ObuChromoSamplePosition.Colocated,
this.uPlane.Height - 1,
out int y0,
out int y1,
out int y1Weight);
ReadOnlySpan<TSample> uRow0;
ReadOnlySpan<TSample> uRow1;
ReadOnlySpan<TSample> vRow0;
ReadOnlySpan<TSample> vRow1;
if (typeof(TSample) == typeof(byte))
{
uRow0 = MemoryMarshal.Cast<byte, TSample>(this.uPlane.DangerousGetRowSpan(y0));
uRow1 = MemoryMarshal.Cast<byte, TSample>(this.uPlane.DangerousGetRowSpan(y1));
vRow0 = MemoryMarshal.Cast<byte, TSample>(this.vPlane.DangerousGetRowSpan(y0));
vRow1 = MemoryMarshal.Cast<byte, TSample>(this.vPlane.DangerousGetRowSpan(y1));
}
else
{
uRow0 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y0, this.subX, this.subY));
uRow1 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y1, this.subX, this.subY));
vRow0 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y0, this.subX, this.subY));
vRow1 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y1, this.subX, this.subY));
}
Span<float> scratch0 = span.Slice(width * 3, width);
Span<float> scratch1 = span.Slice(width * 4, width);
bool isCenteredX = this.subX != 0 && (this.subY == 0 || this.chromaSamplePosition == ObuChromoSamplePosition.Unknown);
ReconstructChromaRow<TSample, TLoader>(uRow0, uRow1, y1Weight, this.subX, isCenteredX, green, scratch0, scratch1);
ReconstructChromaRow<TSample, TLoader>(vRow0, vRow1, y1Weight, this.subX, isCenteredX, blue, scratch0, scratch1);
packedOffset = width * 5;
}
this.colorConverter.ConvertToRgbInPlace(red, green, blue);
Span<float> packedStorage = span[packedOffset..];
Span<TPixel> destination = this.image.PixelBuffer.DangerousGetRowSpan(y);
if (typeof(TSample) == typeof(byte))
{
Span<Rgba32> packed = MemoryMarshal.Cast<float, Rgba32>(packedStorage)[..width];
PackRgba32(red, green, blue, packed);
PixelOperations<TPixel>.Instance.FromRgba32(this.configuration, packed, destination);
}
else
{
Span<Rgba64> packed = MemoryMarshal.Cast<float, Rgba64>(packedStorage)[..width];
PackRgba64(red, green, blue, packed);
PixelOperations<TPixel>.Instance.FromRgba64(this.configuration, packed, destination);
}
}
}
/// <summary>
/// Converts one source row, or one vertically subsampled row pair, using reusable pooled component storage.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <typeparam name="TSample">The encoded sample type.</typeparam>
/// <typeparam name="TStorer">The SIMD narrowing and storage operations for the sample type.</typeparam>
private readonly struct RgbToYuvRowConverter<TPixel, TSample, TStorer>
where TPixel : unmanaged, IPixel<TPixel>
where TSample : unmanaged
where TStorer : struct, ISampleStorer<TSample>
{
/// <summary>
/// The configuration used by bulk pixel conversion.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The destination AV1 frame.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// The source image frame.
/// </summary>
private readonly ImageFrame<TPixel> image;
/// <summary>
/// The full-resolution luma plane.
/// </summary>
private readonly Buffer2DRegion<byte> yPlane;
/// <summary>
/// The blue-difference plane when the frame contains chroma.
/// </summary>
private readonly Buffer2DRegion<byte> uPlane;
/// <summary>
/// The red-difference plane when the frame contains chroma.
/// </summary>
private readonly Buffer2DRegion<byte> vPlane;
/// <summary>
/// The frame-scoped color-model converter.
/// </summary>
private readonly Av1ColorConverterBase colorConverter;
/// <summary>
/// The largest value represented by the encoded AV1 bit depth.
/// </summary>
private readonly float sampleMaximum;
/// <summary>
/// Whether the destination contains only a luma plane.
/// </summary>
private readonly bool isMonochrome;
/// <summary>
/// The horizontal chroma subsampling shift.
/// </summary>
private readonly int subX;
/// <summary>
/// The vertical chroma subsampling shift.
/// </summary>
private readonly int rowShift;
/// <summary>
/// Initializes a new instance of the <see cref="RgbToYuvRowConverter{TPixel, TSample, TStorer}"/> struct.
/// </summary>
/// <param name="configuration">The configuration used for packed-pixel conversion.</param>
/// <param name="frameBuffer">The destination AV1 frame.</param>
/// <param name="image">The source image frame.</param>
/// <param name="colorConverter">The selected H.273 color converter.</param>
/// <param name="sampleMaximum">The largest encoded sample value.</param>
public RgbToYuvRowConverter(
Configuration configuration,
Av1FrameBuffer<byte> frameBuffer,
ImageFrame<TPixel> image,
Av1ColorConverterBase colorConverter,
float sampleMaximum)
{
this.configuration = configuration;
this.frameBuffer = frameBuffer;
this.image = image;
this.colorConverter = colorConverter;
this.sampleMaximum = sampleMaximum;
this.isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
this.subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0;
this.rowShift = !this.isMonochrome && frameBuffer.ColorConfig.SubSamplingY ? 1 : 0;
this.yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0);
this.uPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, this.subX, this.rowShift);
this.vPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, this.subX, this.rowShift);
}
/// <summary>
/// Gets the number of float elements required by the reusable conversion buffer.
/// </summary>
public int ComponentBufferLength
{
get
{
// Three planar rows hold the converted YUV values, and 4:2:0 keeps a second set until its
// chroma has been averaged with the first.
int componentRowCount = this.rowShift == 0 ? 3 : 6;
return this.image.Width * componentRowCount;
}
}
/// <summary>
/// Converts one source row, or one vertically subsampled row pair, to AV1 planes.
/// </summary>
/// <param name="y">The conversion iteration index.</param>
/// <param name="packed">The reusable high-bit-depth RGB staging row.</param>
/// <param name="components">The reusable planar component buffer.</param>
public void Convert(int y, Span<Rgb48> packed, Span<float> components)
{
int width = this.image.Width;
int sourceY = y << this.rowShift;
Span<float> yRow0 = components[..width];
Span<float> cbRow0 = components.Slice(width, width);
Span<float> crRow0 = components.Slice(width * 2, width);
this.ConvertSourceRow(sourceY, packed, yRow0, cbRow0, crRow0);
WriteSamples<TSample, TStorer>(
yRow0,
this.GetPlaneRow(Av1Plane.Y, sourceY),
this.colorConverter.LumaScale,
this.colorConverter.LumaBias,
this.sampleMaximum);
bool hasSecondSourceRow = this.rowShift != 0 && sourceY + 1 < this.image.Height;
Span<float> yRow1 = Span<float>.Empty;
Span<float> cbRow1 = Span<float>.Empty;
Span<float> crRow1 = Span<float>.Empty;
if (hasSecondSourceRow)
{
yRow1 = components.Slice(width * 3, width);
cbRow1 = components.Slice(width * 4, width);
crRow1 = components.Slice(width * 5, width);
this.ConvertSourceRow(sourceY + 1, packed, yRow1, cbRow1, crRow1);
WriteSamples<TSample, TStorer>(
yRow1,
this.GetPlaneRow(Av1Plane.Y, sourceY + 1),
this.colorConverter.LumaScale,
this.colorConverter.LumaBias,
this.sampleMaximum);
}
if (this.isMonochrome)
{
return;
}
Span<TSample> uDestination = this.GetPlaneRow(Av1Plane.U, y);
Span<TSample> vDestination = this.GetPlaneRow(Av1Plane.V, y);
float chromaScale = this.colorConverter.ChromaScale;
float chromaBias = this.colorConverter.ChromaBias;
if (this.subX == 0)
{
WriteSamples<TSample, TStorer>(cbRow0, uDestination, chromaScale, chromaBias, this.sampleMaximum);
WriteSamples<TSample, TStorer>(crRow0, vDestination, chromaScale, chromaBias, this.sampleMaximum);
}
else
{
WriteSubsampledSamples<TSample, TStorer>(cbRow0, cbRow1, uDestination, chromaScale, chromaBias, this.sampleMaximum);
WriteSubsampledSamples<TSample, TStorer>(crRow0, crRow1, vDestination, chromaScale, chromaBias, this.sampleMaximum);
}
}
/// <summary>
/// Converts one source row into normalized planar AV1 components.
/// </summary>
/// <param name="y">The source row index.</param>
/// <param name="packed">The high-bit-depth RGB staging row.</param>
/// <param name="luma">The destination luma values.</param>
/// <param name="chromaBlue">The destination blue-difference values.</param>
/// <param name="chromaRed">The destination red-difference values.</param>
private void ConvertSourceRow(int y, Span<Rgb48> packed, Span<float> luma, Span<float> chromaBlue, Span<float> chromaRed)
{
ReadOnlySpan<TPixel> source = this.image.PixelBuffer.DangerousGetRowSpan(y);
if (typeof(TSample) == typeof(byte))
{
// This is the same planar input contract used by JPEG encoding. Pixel types with optimized
// unpackers reach their existing SIMD path before the AV1 operator consumes the planes.
PixelOperations<TPixel>.Instance.UnpackIntoRgbPlanes(luma, chromaBlue, chromaRed, source);
this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, ByteMaximum);
}
else
{
// JPEG's planar unpack contract is eight-bit. AV1 10/12-bit encoding stages Rgb48 instead
// so high-precision source pixels are not truncated before the color transform.
PixelOperations<TPixel>.Instance.ToRgb48(this.configuration, source, packed);
DeinterleaveRgb48(packed, luma, chromaBlue, chromaRed);
this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, UShortMaximum);
}
}
/// <summary>
/// Gets a writable row from an eight-bit or high-bit-depth AV1 plane.
/// </summary>
/// <param name="plane">The requested plane.</param>
/// <param name="y">The row index in the requested plane.</param>
/// <returns>The writable sample row.</returns>
private Span<TSample> GetPlaneRow(Av1Plane plane, int y)
{
if (typeof(TSample) == typeof(byte))
{
Buffer2DRegion<byte> region = plane switch
{
Av1Plane.Y => this.yPlane,
Av1Plane.U => this.uPlane,
_ => this.vPlane,
};
return MemoryMarshal.Cast<byte, TSample>(region.DangerousGetRowSpan(y));
}
int subX = plane == Av1Plane.Y ? 0 : this.subX;
int subY = plane == Av1Plane.Y ? 0 : this.rowShift;
return MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(plane, y, subX, subY));
}
}
}

156
src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowOperation.cs

@ -1,156 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <content>
/// Provides the pooled parallel row operation used by AV1 YUV-to-RGB decoding.
/// </content>
internal static partial class Av1YuvConverter
{
/// <summary>
/// Converts one reconstructed AV1 row using worker-owned pooled component, chroma, and packed-pixel storage.
/// </summary>
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
/// <typeparam name="TSample">The reconstructed sample type.</typeparam>
/// <typeparam name="TLoader">The SIMD widening operations for the sample type.</typeparam>
private readonly struct YuvToRgbRowOperation<TPixel, TSample, TLoader> : IRowOperation<float>
where TPixel : unmanaged, IPixel<TPixel>
where TSample : unmanaged
where TLoader : struct, ISampleLoader<TSample>
{
private readonly Configuration configuration;
private readonly Av1FrameBuffer<byte> frameBuffer;
private readonly ImageFrame<TPixel> image;
private readonly Buffer2DRegion<byte> yPlane;
private readonly Buffer2DRegion<byte> uPlane;
private readonly Buffer2DRegion<byte> vPlane;
private readonly YuvToRgbParameters parameters;
private readonly ConversionMode mode;
private readonly ObuChromoSamplePosition chromaSamplePosition;
private readonly bool isMonochrome;
private readonly int subX;
private readonly int subY;
/// <summary>
/// Initializes a new instance of the <see cref="YuvToRgbRowOperation{TPixel, TSample, TLoader}"/> struct.
/// </summary>
/// <param name="configuration">The configuration used for packed-pixel conversion.</param>
/// <param name="frameBuffer">The reconstructed AV1 frame.</param>
/// <param name="image">The destination image frame.</param>
/// <param name="mode">The resolved H.273 conversion mode.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
public YuvToRgbRowOperation(
Configuration configuration,
Av1FrameBuffer<byte> frameBuffer,
ImageFrame<TPixel> image,
ConversionMode mode,
in YuvToRgbParameters parameters)
{
this.configuration = configuration;
this.frameBuffer = frameBuffer;
this.image = image;
this.mode = mode;
this.parameters = parameters;
this.isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
this.subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0;
this.subY = frameBuffer.ColorConfig.SubSamplingY ? 1 : 0;
this.chromaSamplePosition = frameBuffer.ColorConfig.ChromaSamplePosition;
this.yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0);
this.uPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, this.subX, this.subY);
this.vPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, this.subX, this.subY);
}
/// <inheritdoc/>
public int GetRequiredBufferLength(Rectangle bounds)
{
// Three float component rows are converted in place. Color input adds two reusable chroma scratch
// rows, and the final one or two float-sized slots per pixel back an Rgba32 or Rgba64 staging row.
int rowCount = this.isMonochrome ? 3 : 5;
int packedRowCount = typeof(TSample) == typeof(byte) ? 1 : 2;
return bounds.Width * (rowCount + packedRowCount);
}
/// <inheritdoc/>
public void Invoke(int y, Span<float> span)
{
int width = this.image.Width;
Span<float> red = span[..width];
Span<float> green = span.Slice(width, width);
Span<float> blue = span.Slice(width * 2, width);
ReadOnlySpan<TSample> ySource;
if (typeof(TSample) == typeof(byte))
{
ySource = MemoryMarshal.Cast<byte, TSample>(this.yPlane.DangerousGetRowSpan(y));
}
else
{
ySource = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0));
}
ConvertSamplesToFloat<TSample, TLoader>(ySource, red);
int packedOffset = width * 3;
if (!this.isMonochrome)
{
GetChromaCoordinates(
y,
this.subY,
this.subY != 0 && this.chromaSamplePosition != ObuChromoSamplePosition.Colocated,
this.uPlane.Height - 1,
out int y0,
out int y1,
out int y1Weight);
ReadOnlySpan<TSample> uRow0;
ReadOnlySpan<TSample> uRow1;
ReadOnlySpan<TSample> vRow0;
ReadOnlySpan<TSample> vRow1;
if (typeof(TSample) == typeof(byte))
{
uRow0 = MemoryMarshal.Cast<byte, TSample>(this.uPlane.DangerousGetRowSpan(y0));
uRow1 = MemoryMarshal.Cast<byte, TSample>(this.uPlane.DangerousGetRowSpan(y1));
vRow0 = MemoryMarshal.Cast<byte, TSample>(this.vPlane.DangerousGetRowSpan(y0));
vRow1 = MemoryMarshal.Cast<byte, TSample>(this.vPlane.DangerousGetRowSpan(y1));
}
else
{
uRow0 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y0, this.subX, this.subY));
uRow1 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y1, this.subX, this.subY));
vRow0 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y0, this.subX, this.subY));
vRow1 = MemoryMarshal.Cast<ushort, TSample>(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y1, this.subX, this.subY));
}
Span<float> scratch0 = span.Slice(width * 3, width);
Span<float> scratch1 = span.Slice(width * 4, width);
bool isCenteredX = this.subX != 0 && (this.subY == 0 || this.chromaSamplePosition == ObuChromoSamplePosition.Unknown);
ReconstructChromaRow<TSample, TLoader>(uRow0, uRow1, y1Weight, this.subX, isCenteredX, green, scratch0, scratch1);
ReconstructChromaRow<TSample, TLoader>(vRow0, vRow1, y1Weight, this.subX, isCenteredX, blue, scratch0, scratch1);
packedOffset = width * 5;
}
ConvertYuvToRgbRow(red, green, blue, this.isMonochrome, this.mode, in this.parameters);
Span<float> packedStorage = span[packedOffset..];
Span<TPixel> destination = this.image.PixelBuffer.DangerousGetRowSpan(y);
if (typeof(TSample) == typeof(byte))
{
Span<Rgba32> packed = MemoryMarshal.Cast<float, Rgba32>(packedStorage)[..width];
PackRgba32(red, green, blue, packed);
PixelOperations<TPixel>.Instance.FromRgba32(this.configuration, packed, destination);
}
else
{
Span<Rgba64> packed = MemoryMarshal.Cast<float, Rgba64>(packedStorage)[..width];
PackRgba64(red, green, blue, packed);
PixelOperations<TPixel>.Instance.FromRgba64(this.configuration, packed, destination);
}
}
}
}

409
src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs

@ -10,7 +10,7 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
/// <content>
/// Provides SIMD sample widening, chroma reconstruction, color traversal, and packed output for AV1 decoding.
/// Provides SIMD sample widening, chroma reconstruction, planar storage, and packed output for AV1 color conversion.
/// </content>
internal static partial class Av1YuvConverter
{
@ -43,6 +43,35 @@ internal static partial class Av1YuvConverter
public static abstract Vector512<float> LoadVector512(ref TSample source);
}
/// <summary>
/// Defines the SIMD narrowing and storage operations for one encoded AV1 sample type.
/// </summary>
/// <typeparam name="TSample">The encoded sample type.</typeparam>
private interface ISampleStorer<TSample>
where TSample : unmanaged
{
/// <summary>
/// Narrows and stores four integer samples.
/// </summary>
/// <param name="source">The integer samples.</param>
/// <param name="destination">The first destination sample.</param>
public static abstract void Store(Vector128<int> source, ref TSample destination);
/// <summary>
/// Narrows and stores eight integer samples.
/// </summary>
/// <param name="source">The integer samples.</param>
/// <param name="destination">The first destination sample.</param>
public static abstract void Store(Vector256<int> source, ref TSample destination);
/// <summary>
/// Narrows and stores sixteen integer samples.
/// </summary>
/// <param name="source">The integer samples.</param>
/// <param name="destination">The first destination sample.</param>
public static abstract void Store(Vector512<int> source, ref TSample destination);
}
/// <summary>
/// Widens reconstructed integer samples into a pooled float component row.
/// </summary>
@ -357,158 +386,218 @@ internal static partial class Av1YuvConverter
}
/// <summary>
/// Dispatches one normalized component row to its matrix-specific scalar and SIMD operator.
/// Deinterleaves high-bit-depth RGB pixels into planar component rows.
/// </summary>
/// <param name="red">The luma row, replaced by red.</param>
/// <param name="green">The blue-difference row, replaced by green.</param>
/// <param name="blue">The red-difference row, replaced by blue.</param>
/// <param name="isMonochrome">Whether the frame contains only luma samples.</param>
/// <param name="mode">The resolved H.273 conversion mode.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
private static void ConvertYuvToRgbRow(
Span<float> red,
Span<float> green,
Span<float> blue,
bool isMonochrome,
ConversionMode mode,
in YuvToRgbParameters parameters)
/// <param name="source">The packed RGBA pixels.</param>
/// <param name="red">The destination red components.</param>
/// <param name="green">The destination green components.</param>
/// <param name="blue">The destination blue components.</param>
private static void DeinterleaveRgb48(ReadOnlySpan<Rgb48> source, Span<float> red, Span<float> green, Span<float> blue)
{
if (isMonochrome)
ref float redBase = ref MemoryMarshal.GetReference(red);
ref float greenBase = ref MemoryMarshal.GetReference(green);
ref float blueBase = ref MemoryMarshal.GetReference(blue);
ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source);
int length = source.Length;
int i = 0;
if (Vector128.IsHardwareAccelerated)
{
ConvertYuvToRgbRow<MonochromeOperator>(red, green, blue, in parameters);
return;
int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
ref Rgb48 pixel0 = ref Unsafe.Add(ref sourceBase, i);
ref Rgb48 pixel1 = ref Unsafe.Add(ref sourceBase, i + 1);
ref Rgb48 pixel2 = ref Unsafe.Add(ref sourceBase, i + 2);
ref Rgb48 pixel3 = ref Unsafe.Add(ref sourceBase, i + 3);
// Widen each UInt16 channel to a UInt32 lane before converting to Single. This avoids
// reinterpreting adjacent 16-bit samples as one unrelated 32-bit integer.
Vector128<float> redVector = Vector128.ConvertToSingle(
Vector128.Create((uint)pixel0.R, pixel1.R, pixel2.R, pixel3.R));
Vector128<float> greenVector = Vector128.ConvertToSingle(
Vector128.Create((uint)pixel0.G, pixel1.G, pixel2.G, pixel3.G));
Vector128<float> blueVector = Vector128.ConvertToSingle(
Vector128.Create((uint)pixel0.B, pixel1.B, pixel2.B, pixel3.B));
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref redBase, i)) = redVector;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref greenBase, i)) = greenVector;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref blueBase, i)) = blueVector;
}
}
switch (mode)
{
case ConversionMode.Identity:
ConvertYuvToRgbRow<IdentityOperator>(red, green, blue, in parameters);
break;
case ConversionMode.YCgCo:
ConvertYuvToRgbRow<YCgCoOperator>(red, green, blue, in parameters);
break;
case ConversionMode.Smpte2085:
ConvertYuvToRgbRow<Smpte2085Operator>(red, green, blue, in parameters);
break;
case ConversionMode.ConstantLuminance:
ConvertYuvToRgbRow<ConstantLuminanceOperator>(red, green, blue, in parameters);
break;
case ConversionMode.ICtCp when parameters.TransferCharacteristics == OpenBitstreamUnit.ObuTransferCharacteristics.Hlg:
ConvertYuvToRgbRow<ICtCpHlgOperator>(red, green, blue, in parameters);
break;
case ConversionMode.ICtCp:
ConvertYuvToRgbRow<ICtCpOperator>(red, green, blue, in parameters);
break;
default:
ConvertYuvToRgbRow<CoefficientsOperator>(red, green, blue, in parameters);
break;
for (; i < length; i++)
{
Rgb48 pixel = Unsafe.Add(ref sourceBase, i);
Unsafe.Add(ref redBase, i) = pixel.R;
Unsafe.Add(ref greenBase, i) = pixel.G;
Unsafe.Add(ref blueBase, i) = pixel.B;
}
}
/// <summary>
/// Converts one component row using the selected static scalar and SIMD operator.
/// Scales, quantizes, and stores one planar row using the widest available SIMD width.
/// </summary>
/// <typeparam name="TOperator">The matrix-specific conversion operator.</typeparam>
/// <param name="red">The luma row, replaced by red.</param>
/// <param name="green">The blue-difference row, replaced by green.</param>
/// <param name="blue">The red-difference row, replaced by blue.</param>
/// <param name="parameters">The resolved H.273 conversion parameters.</param>
private static void ConvertYuvToRgbRow<TOperator>(Span<float> red, Span<float> green, Span<float> blue, in YuvToRgbParameters parameters)
where TOperator : struct, IYuvToRgbOperator
/// <typeparam name="TSample">The encoded sample type.</typeparam>
/// <typeparam name="TStorer">The narrowing and storage operations for the sample type.</typeparam>
/// <param name="source">The normalized source values.</param>
/// <param name="destination">The encoded destination samples.</param>
/// <param name="scale">The encoded range scale.</param>
/// <param name="bias">The encoded range bias.</param>
/// <param name="maximum">The largest encoded sample value.</param>
private static void WriteSamples<TSample, TStorer>(ReadOnlySpan<float> source, Span<TSample> destination, float scale, float bias, float maximum)
where TSample : unmanaged
where TStorer : struct, ISampleStorer<TSample>
{
ref float redBase = ref MemoryMarshal.GetReference(red);
ref float greenBase = ref MemoryMarshal.GetReference(green);
ref float blueBase = ref MemoryMarshal.GetReference(blue);
int length = red.Length;
ref float sourceBase = ref MemoryMarshal.GetReference(source);
ref TSample destinationBase = ref MemoryMarshal.GetReference(destination);
int length = destination.Length;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
int oneVectorFromEnd = length - Vector512<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
Vector512<float> values = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref sourceBase, i));
TStorer.Store(ScaleBiasRoundAndClampToInt32(values, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
Vector256<float> values = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref sourceBase, i));
TStorer.Store(ScaleBiasRoundAndClampToInt32(values, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i));
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
Vector128<float> values = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref sourceBase, i));
TStorer.Store(ScaleBiasRoundAndClampToInt32(values, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i));
}
}
for (; i < length; i++)
{
Unsafe.Add(ref destinationBase, i) = ToSample<TSample>((Unsafe.Add(ref sourceBase, i) * scale) + bias, maximum);
}
}
/// <summary>
/// Averages one or two planar rows into horizontally subsampled encoded samples.
/// </summary>
/// <typeparam name="TSample">The encoded sample type.</typeparam>
/// <typeparam name="TStorer">The narrowing and storage operations for the sample type.</typeparam>
/// <param name="row0">The first normalized source row.</param>
/// <param name="row1">The optional second normalized source row.</param>
/// <param name="destination">The encoded subsampled destination row.</param>
/// <param name="scale">The encoded range scale.</param>
/// <param name="bias">The encoded range bias.</param>
/// <param name="maximum">The largest encoded sample value.</param>
private static void WriteSubsampledSamples<TSample, TStorer>(
ReadOnlySpan<float> row0,
ReadOnlySpan<float> row1,
Span<TSample> destination,
float scale,
float bias,
float maximum)
where TSample : unmanaged
where TStorer : struct, ISampleStorer<TSample>
{
ref float row0Base = ref MemoryMarshal.GetReference(row0);
ref float row1Base = ref MemoryMarshal.GetReference(row1);
ref TSample destinationBase = ref MemoryMarshal.GetReference(destination);
bool hasSecondRow = !row1.IsEmpty;
float averageScale = hasSecondRow ? 0.25F : 0.5F;
int length = row0.Length;
int i = 0;
if (Vector512.IsHardwareAccelerated)
{
Vector512<float> lumaBias = Vector512.Create(parameters.LumaBias);
Vector512<float> inverseLumaScale = Vector512.Create(1F / parameters.LumaScale);
Vector512<float> chromaBias = Vector512.Create(parameters.ChromaBias);
Vector512<float> inverseChromaScale = Vector512.Create(1F / parameters.ChromaScale);
Vector512<int> evenOdd = Vector512.Create(0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15);
int oneVectorFromEnd = length - Vector512<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector512<float>.Count)
{
ref Vector512<float> redVector = ref Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref redBase, i));
Vector512<float> y = (redVector - lumaBias) * inverseLumaScale;
Vector512<float> cb = default;
Vector512<float> cr = default;
if (TOperator.UsesChroma)
Vector512<float> sum = Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref row0Base, i));
if (hasSecondRow)
{
cb = (Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale;
cr = (Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale;
sum += Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref row1Base, i));
}
TOperator.Convert(ref y, ref cb, ref cr, in parameters);
redVector = y;
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref greenBase, i)) = cb;
Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref blueBase, i)) = cr;
Vector512<float> shuffled = Vector512.Shuffle(sum, evenOdd);
Vector256<float> average = (shuffled.GetLower() + shuffled.GetUpper()) * Vector256.Create(averageScale);
TStorer.Store(ScaleBiasRoundAndClampToInt32(average, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i >> 1));
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<float> lumaBias = Vector256.Create(parameters.LumaBias);
Vector256<float> inverseLumaScale = Vector256.Create(1F / parameters.LumaScale);
Vector256<float> chromaBias = Vector256.Create(parameters.ChromaBias);
Vector256<float> inverseChromaScale = Vector256.Create(1F / parameters.ChromaScale);
Vector256<int> evenOdd = Vector256.Create(0, 2, 4, 6, 1, 3, 5, 7);
int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
ref Vector256<float> redVector = ref Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref redBase, i));
Vector256<float> y = (redVector - lumaBias) * inverseLumaScale;
Vector256<float> cb = default;
Vector256<float> cr = default;
if (TOperator.UsesChroma)
Vector256<float> sum = Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref row0Base, i));
if (hasSecondRow)
{
cb = (Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale;
cr = (Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale;
sum += Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref row1Base, i));
}
TOperator.Convert(ref y, ref cb, ref cr, in parameters);
redVector = y;
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref greenBase, i)) = cb;
Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref blueBase, i)) = cr;
Vector256<float> shuffled = Vector256.Shuffle(sum, evenOdd);
Vector128<float> average = (shuffled.GetLower() + shuffled.GetUpper()) * Vector128.Create(averageScale);
TStorer.Store(ScaleBiasRoundAndClampToInt32(average, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i >> 1));
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<float> lumaBias = Vector128.Create(parameters.LumaBias);
Vector128<float> inverseLumaScale = Vector128.Create(1F / parameters.LumaScale);
Vector128<float> chromaBias = Vector128.Create(parameters.ChromaBias);
Vector128<float> inverseChromaScale = Vector128.Create(1F / parameters.ChromaScale);
int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
Vector128<int> evenOdd = Vector128.Create(0, 2, 1, 3);
int twoVectorsFromEnd = length - (Vector128<float>.Count * 2);
for (; i <= twoVectorsFromEnd; i += Vector128<float>.Count * 2)
{
ref Vector128<float> redVector = ref Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref redBase, i));
Vector128<float> y = (redVector - lumaBias) * inverseLumaScale;
Vector128<float> cb = default;
Vector128<float> cr = default;
if (TOperator.UsesChroma)
Vector128<float> sum0 = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref row0Base, i));
Vector128<float> sum1 = Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref row0Base, i + Vector128<float>.Count));
if (hasSecondRow)
{
cb = (Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref greenBase, i)) - chromaBias) * inverseChromaScale;
cr = (Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref blueBase, i)) - chromaBias) * inverseChromaScale;
sum0 += Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref row1Base, i));
sum1 += Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref row1Base, i + Vector128<float>.Count));
}
TOperator.Convert(ref y, ref cb, ref cr, in parameters);
redVector = y;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref greenBase, i)) = cb;
Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref blueBase, i)) = cr;
Vector128<float> shuffled0 = Vector128.Shuffle(sum0, evenOdd);
Vector128<float> shuffled1 = Vector128.Shuffle(sum1, evenOdd);
Vector64<float> average0 = (shuffled0.GetLower() + shuffled0.GetUpper()) * Vector64.Create(averageScale);
Vector64<float> average1 = (shuffled1.GetLower() + shuffled1.GetUpper()) * Vector64.Create(averageScale);
Vector128<float> average = Vector128.Create(average0, average1);
TStorer.Store(ScaleBiasRoundAndClampToInt32(average, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i >> 1));
}
}
for (; i < length; i++)
for (; i < length; i += 2)
{
float y = (Unsafe.Add(ref redBase, i) - parameters.LumaBias) / parameters.LumaScale;
float cb = TOperator.UsesChroma ? (Unsafe.Add(ref greenBase, i) - parameters.ChromaBias) / parameters.ChromaScale : 0F;
float cr = TOperator.UsesChroma ? (Unsafe.Add(ref blueBase, i) - parameters.ChromaBias) / parameters.ChromaScale : 0F;
TOperator.Convert(ref y, ref cb, ref cr, in parameters);
Unsafe.Add(ref redBase, i) = y;
Unsafe.Add(ref greenBase, i) = cb;
Unsafe.Add(ref blueBase, i) = cr;
int columnCount = Math.Min(2, length - i);
float sum = Unsafe.Add(ref row0Base, i);
if (columnCount == 2)
{
sum += Unsafe.Add(ref row0Base, i + 1);
}
if (hasSecondRow)
{
sum += Unsafe.Add(ref row1Base, i);
if (columnCount == 2)
{
sum += Unsafe.Add(ref row1Base, i + 1);
}
}
float sampleCount = columnCount * (hasSecondRow ? 2 : 1);
Unsafe.Add(ref destinationBase, i >> 1) = ToSample<TSample>(((sum / sampleCount) * scale) + bias, maximum);
}
}
@ -643,6 +732,51 @@ internal static partial class Av1YuvConverter
}
}
/// <summary>
/// Applies the encoded component range and converts four lanes to bounded integer samples.
/// </summary>
/// <param name="value">The normalized component values.</param>
/// <param name="scale">The encoded range scale.</param>
/// <param name="bias">The encoded range bias.</param>
/// <param name="maximum">The largest encoded sample value.</param>
/// <returns>The bounded integer samples.</returns>
private static Vector128<int> ScaleBiasRoundAndClampToInt32(Vector128<float> value, float scale, float bias, float maximum)
{
Vector128<float> encoded = (value * Vector128.Create(scale)) + Vector128.Create(bias);
Vector128<float> bounded = Vector128.Clamp(encoded, Vector128<float>.Zero, Vector128.Create(maximum));
return Vector128.ConvertToInt32(Vector128.Round(bounded, MidpointRounding.AwayFromZero));
}
/// <summary>
/// Applies the encoded component range and converts eight lanes to bounded integer samples.
/// </summary>
/// <param name="value">The normalized component values.</param>
/// <param name="scale">The encoded range scale.</param>
/// <param name="bias">The encoded range bias.</param>
/// <param name="maximum">The largest encoded sample value.</param>
/// <returns>The bounded integer samples.</returns>
private static Vector256<int> ScaleBiasRoundAndClampToInt32(Vector256<float> value, float scale, float bias, float maximum)
{
Vector256<float> encoded = (value * Vector256.Create(scale)) + Vector256.Create(bias);
Vector256<float> bounded = Vector256.Clamp(encoded, Vector256<float>.Zero, Vector256.Create(maximum));
return Vector256.ConvertToInt32(Vector256.Round(bounded, MidpointRounding.AwayFromZero));
}
/// <summary>
/// Applies the encoded component range and converts sixteen lanes to bounded integer samples.
/// </summary>
/// <param name="value">The normalized component values.</param>
/// <param name="scale">The encoded range scale.</param>
/// <param name="bias">The encoded range bias.</param>
/// <param name="maximum">The largest encoded sample value.</param>
/// <returns>The bounded integer samples.</returns>
private static Vector512<int> ScaleBiasRoundAndClampToInt32(Vector512<float> value, float scale, float bias, float maximum)
{
Vector512<float> encoded = (value * Vector512.Create(scale)) + Vector512.Create(bias);
Vector512<float> bounded = Vector512.Clamp(encoded, Vector512<float>.Zero, Vector512.Create(maximum));
return Vector512.ConvertToInt32(Vector512.Round(bounded, MidpointRounding.AwayFromZero));
}
/// <summary>
/// Scales, rounds, and clamps four normalized components to integer storage values.
/// </summary>
@ -787,4 +921,63 @@ internal static partial class Av1YuvConverter
return Vector512.ConvertToSingle(Vector512.Create(lower32, upper32));
}
}
/// <summary>
/// Narrows encoded integer lanes to eight-bit samples.
/// </summary>
private readonly struct ByteSampleStorer : ISampleStorer<byte>
{
/// <inheritdoc/>
public static void Store(Vector128<int> source, ref byte destination)
{
Vector128<ushort> samples16 = Vector128.Narrow(source.AsUInt32(), Vector128<uint>.Zero);
Vector128<byte> samples8 = Vector128.Narrow(samples16, Vector128<ushort>.Zero);
// The lower four bytes contain the four source lanes after the two narrowing stages.
Unsafe.WriteUnaligned(ref destination, samples8.AsUInt32().ToScalar());
}
/// <inheritdoc/>
public static void Store(Vector256<int> source, ref byte destination)
{
Store(source.GetLower(), ref destination);
Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector128<int>.Count));
}
/// <inheritdoc/>
public static void Store(Vector512<int> source, ref byte destination)
{
Store(source.GetLower(), ref destination);
Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector256<int>.Count));
}
}
/// <summary>
/// Narrows encoded integer lanes to unsigned 16-bit samples.
/// </summary>
private readonly struct UShortSampleStorer : ISampleStorer<ushort>
{
/// <inheritdoc/>
public static void Store(Vector128<int> source, ref ushort destination)
{
Vector128<ushort> samples = Vector128.Narrow(source.AsUInt32(), Vector128<uint>.Zero);
// The lower four UInt16 values are contiguous and can be committed with one unaligned store.
Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref destination), samples.AsUInt64().ToScalar());
}
/// <inheritdoc/>
public static void Store(Vector256<int> source, ref ushort destination)
{
Store(source.GetLower(), ref destination);
Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector128<int>.Count));
}
/// <inheritdoc/>
public static void Store(Vector512<int> source, ref ushort destination)
{
Store(source.GetLower(), ref destination);
Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector256<int>.Count));
}
}
}

824
src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs

@ -26,42 +26,6 @@ internal static partial class Av1YuvConverter
/// </summary>
private const float UShortMaximum = ushort.MaxValue;
/// <summary>
/// Identifies the matrix operation used between encoded planes and RGB components.
/// </summary>
private enum ConversionMode
{
/// <summary>
/// A coefficient-based YCbCr matrix conversion.
/// </summary>
Coefficients,
/// <summary>
/// Direct G, B, and R component mapping from the Y, U, and V planes.
/// </summary>
Identity,
/// <summary>
/// The reversible-style YCgCo color transform.
/// </summary>
YCgCo,
/// <summary>
/// The SMPTE ST 2085 YDzDx color transform.
/// </summary>
Smpte2085,
/// <summary>
/// A constant-luminance transform using the signaled transfer characteristics.
/// </summary>
ConstantLuminance,
/// <summary>
/// The BT.2100 ICtCp color transform.
/// </summary>
ICtCp,
}
/// <summary>
/// Converts the reconstructed YUV planes to packed pixels.
/// </summary>
@ -74,7 +38,7 @@ internal static partial class Av1YuvConverter
{
GetConversionParameters(
frameBuffer,
out ConversionMode mode,
out Av1ColorConversionMode mode,
out float kr,
out float kg,
out float kb,
@ -85,20 +49,41 @@ internal static partial class Av1YuvConverter
out float sampleMaximum);
ObuTransferCharacteristics transferCharacteristics = frameBuffer.ColorConfig.TransferCharacteristics;
ConstantLuminanceScales constantLuminanceScales = mode == ConversionMode.ConstantLuminance
? new ConstantLuminanceScales(transferCharacteristics, kr, kb)
Av1ConstantLuminanceScales constantLuminanceScales = mode == Av1ColorConversionMode.ConstantLuminance
? new Av1ConstantLuminanceScales(transferCharacteristics, kr, kb)
: default;
YuvToRgbParameters parameters = new(kr, kg, kb, transferCharacteristics, in constantLuminanceScales, lumaBias, lumaScale, chromaBias, chromaScale);
Av1ColorConversionParameters parameters = new(
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
lumaBias,
lumaScale,
chromaBias,
chromaScale);
Av1ColorConverterBase colorConverter = Av1ColorConverterBase.Create(mode, in parameters, frameBuffer.ColorFormat == Av1ColorFormat.Yuv400);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{
YuvToRgbRowOperation<TPixel, byte, ByteSampleLoader> operation = new(configuration, frameBuffer, image, mode, in parameters);
ParallelRowIterator.IterateRows<YuvToRgbRowOperation<TPixel, byte, ByteSampleLoader>, float>(configuration, image.Bounds, in operation);
YuvToRgbRowConverter<TPixel, byte, ByteSampleLoader> converter = new(configuration, frameBuffer, image, colorConverter);
using IMemoryOwner<float> owner = configuration.MemoryAllocator.Allocate<float>(converter.BufferLength);
Span<float> scratch = owner.GetSpan();
for (int y = 0; y < image.Height; y++)
{
converter.Convert(y, scratch);
}
}
else
{
YuvToRgbRowOperation<TPixel, ushort, UShortSampleLoader> operation = new(configuration, frameBuffer, image, mode, in parameters);
ParallelRowIterator.IterateRows<YuvToRgbRowOperation<TPixel, ushort, UShortSampleLoader>, float>(configuration, image.Bounds, in operation);
YuvToRgbRowConverter<TPixel, ushort, UShortSampleLoader> converter = new(configuration, frameBuffer, image, colorConverter);
using IMemoryOwner<float> owner = configuration.MemoryAllocator.Allocate<float>(converter.BufferLength);
Span<float> scratch = owner.GetSpan();
for (int y = 0; y < image.Height; y++)
{
converter.Convert(y, scratch);
}
}
}
@ -114,7 +99,7 @@ internal static partial class Av1YuvConverter
{
GetConversionParameters(
frameBuffer,
out ConversionMode mode,
out Av1ColorConversionMode mode,
out float kr,
out float kg,
out float kb,
@ -125,165 +110,45 @@ internal static partial class Av1YuvConverter
out float sampleMaximum);
ObuTransferCharacteristics transferCharacteristics = frameBuffer.ColorConfig.TransferCharacteristics;
ConstantLuminanceScales constantLuminanceScales = mode == ConversionMode.ConstantLuminance
? new ConstantLuminanceScales(transferCharacteristics, kr, kb)
Av1ConstantLuminanceScales constantLuminanceScales = mode == Av1ColorConversionMode.ConstantLuminance
? new Av1ConstantLuminanceScales(transferCharacteristics, kr, kb)
: default;
bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
int subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0;
int subY = frameBuffer.ColorConfig.SubSamplingY ? 1 : 0;
Buffer2DRegion<byte> yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0);
Buffer2DRegion<byte> uPlane = isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, subX, subY);
Buffer2DRegion<byte> vPlane = isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, subX, subY);
int sourceRowsPerIteration = !isMonochrome && subY != 0 ? 2 : 1;
bool isEightBit = frameBuffer.BitDepth == Av1BitDepth.EightBit;
int rowBufferLength = image.Width * sourceRowsPerIteration;
using IMemoryOwner<Rgb24>? rowOwner = isEightBit
? configuration.MemoryAllocator.Allocate<Rgb24>(rowBufferLength)
: null;
using IMemoryOwner<Rgb48>? highBitDepthRowOwner = isEightBit
? null
: configuration.MemoryAllocator.Allocate<Rgb48>(rowBufferLength);
Av1ColorConversionParameters parameters = new(
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
lumaBias,
lumaScale,
chromaBias,
chromaScale);
Span<Rgb24> rgbRow0 = rowOwner is null ? Span<Rgb24>.Empty : rowOwner.GetSpan()[..image.Width];
Span<Rgb24> rgbRow1 = sourceRowsPerIteration == 2 && rowOwner is not null
? rowOwner.GetSpan().Slice(image.Width, image.Width)
: Span<Rgb24>.Empty;
Span<Rgb48> highBitDepthRgbRow0 = highBitDepthRowOwner is null
? Span<Rgb48>.Empty
: highBitDepthRowOwner.GetSpan()[..image.Width];
Span<Rgb48> highBitDepthRgbRow1 = sourceRowsPerIteration == 2 && highBitDepthRowOwner is not null
? highBitDepthRowOwner.GetSpan().Slice(image.Width, image.Width)
: Span<Rgb48>.Empty;
for (int y = 0; y < image.Height; y += sourceRowsPerIteration)
bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
Av1ColorConverterBase colorConverter = Av1ColorConverterBase.Create(mode, in parameters, isMonochrome);
int rowShift = !isMonochrome && frameBuffer.ColorConfig.SubSamplingY ? 1 : 0;
int iterationCount = (image.Height + rowShift) >> rowShift;
if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{
if (isEightBit)
RgbToYuvRowConverter<TPixel, byte, ByteSampleStorer> converter = new(configuration, frameBuffer, image, colorConverter, sampleMaximum);
using IMemoryOwner<float> componentOwner = configuration.MemoryAllocator.Allocate<float>(converter.ComponentBufferLength);
Span<float> components = componentOwner.GetSpan();
for (int y = 0; y < iterationCount; y++)
{
PixelOperations<TPixel>.Instance.ToRgb24(
configuration,
image.PixelBuffer.DangerousGetRowSpan(y),
rgbRow0);
converter.Convert(y, Span<Rgb48>.Empty, components);
}
else
{
// Rgb48 retains source component precision before the values are quantized to the requested
// 10-bit or 12-bit AV1 sample range.
PixelOperations<TPixel>.Instance.ToRgb48(
configuration,
image.PixelBuffer.DangerousGetRowSpan(y),
highBitDepthRgbRow0);
}
bool hasSecondSourceRow = sourceRowsPerIteration == 2 && y + 1 < image.Height;
if (hasSecondSourceRow)
{
if (isEightBit)
{
PixelOperations<TPixel>.Instance.ToRgb24(
configuration,
image.PixelBuffer.DangerousGetRowSpan(y + 1),
rgbRow1);
}
else
{
PixelOperations<TPixel>.Instance.ToRgb48(
configuration,
image.PixelBuffer.DangerousGetRowSpan(y + 1),
highBitDepthRgbRow1);
}
}
if (isEightBit)
{
Span<byte> yRow0 = yPlane.DangerousGetRowSpan(y);
if (isMonochrome || subX == 0)
{
ConvertRgbToYuvRow(
rgbRow0,
yRow0,
isMonochrome ? Span<byte>.Empty : uPlane.DangerousGetRowSpan(y),
isMonochrome ? Span<byte>.Empty : vPlane.DangerousGetRowSpan(y),
mode,
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
lumaBias,
lumaScale,
chromaBias,
chromaScale,
sampleMaximum);
}
else
{
ConvertRgbToSubsampledYuvRows(
rgbRow0,
hasSecondSourceRow ? rgbRow1 : ReadOnlySpan<Rgb24>.Empty,
yRow0,
hasSecondSourceRow ? yPlane.DangerousGetRowSpan(y + 1) : Span<byte>.Empty,
uPlane.DangerousGetRowSpan(y >> subY),
vPlane.DangerousGetRowSpan(y >> subY),
mode,
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
lumaBias,
lumaScale,
chromaBias,
chromaScale,
sampleMaximum);
}
}
else
}
else
{
RgbToYuvRowConverter<TPixel, ushort, UShortSampleStorer> converter = new(configuration, frameBuffer, image, colorConverter, sampleMaximum);
using IMemoryOwner<Rgb48> packedOwner = configuration.MemoryAllocator.Allocate<Rgb48>(image.Width);
using IMemoryOwner<float> componentOwner = configuration.MemoryAllocator.Allocate<float>(converter.ComponentBufferLength);
Span<Rgb48> packed = packedOwner.GetSpan()[..image.Width];
Span<float> components = componentOwner.GetSpan();
for (int y = 0; y < iterationCount; y++)
{
Span<ushort> yRow0 = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0);
if (isMonochrome || subX == 0)
{
ConvertRgbToYuvRow(
highBitDepthRgbRow0,
yRow0,
isMonochrome ? Span<ushort>.Empty : frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y, 0, 0),
isMonochrome ? Span<ushort>.Empty : frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y, 0, 0),
mode,
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
lumaBias,
lumaScale,
chromaBias,
chromaScale,
sampleMaximum);
}
else
{
ConvertRgbToSubsampledYuvRows(
highBitDepthRgbRow0,
hasSecondSourceRow ? highBitDepthRgbRow1 : ReadOnlySpan<Rgb48>.Empty,
yRow0,
hasSecondSourceRow ? frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y + 1, 0, 0) : Span<ushort>.Empty,
frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y >> subY, subX, subY),
frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y >> subY, subX, subY),
mode,
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
lumaBias,
lumaScale,
chromaBias,
chromaScale,
sampleMaximum);
}
converter.Convert(y, packed, components);
}
}
}
@ -303,7 +168,7 @@ internal static partial class Av1YuvConverter
/// <param name="sampleMaximum">The largest encoded sample value.</param>
private static void GetConversionParameters(
Av1FrameBuffer<byte> frameBuffer,
out ConversionMode mode,
out Av1ColorConversionMode mode,
out float kr,
out float kg,
out float kb,
@ -313,7 +178,7 @@ internal static partial class Av1YuvConverter
out float chromaScale,
out float sampleMaximum)
{
mode = ConversionMode.Coefficients;
mode = Av1ColorConversionMode.Coefficients;
kr = 0F;
kb = 0F;
@ -321,7 +186,7 @@ internal static partial class Av1YuvConverter
switch (frameBuffer.ColorConfig.MatrixCoefficients)
{
case ObuMatrixCoefficients.Identity:
mode = ConversionMode.Identity;
mode = Av1ColorConversionMode.Identity;
break;
case ObuMatrixCoefficients.Bt709:
kr = 0.2126F;
@ -343,29 +208,29 @@ internal static partial class Av1YuvConverter
kb = 0.087F;
break;
case ObuMatrixCoefficients.SmpteYCgCo:
mode = ConversionMode.YCgCo;
mode = Av1ColorConversionMode.YCgCo;
break;
case ObuMatrixCoefficients.Bt2020NonConstantLuminance:
kr = 0.2627F;
kb = 0.0593F;
break;
case ObuMatrixCoefficients.Bt2020ConstantLuminance:
mode = ConversionMode.ConstantLuminance;
mode = Av1ColorConversionMode.ConstantLuminance;
kr = 0.2627F;
kb = 0.0593F;
break;
case ObuMatrixCoefficients.Smpte2085:
mode = ConversionMode.Smpte2085;
mode = Av1ColorConversionMode.Smpte2085;
break;
case ObuMatrixCoefficients.ChromaticityDerivedNonConstantLuminance:
GetChromaticityDerivedCoefficients(frameBuffer.ColorConfig.ColorPrimaries, out kr, out kb);
break;
case ObuMatrixCoefficients.ChromaticityDerivedConstantLuminance:
mode = ConversionMode.ConstantLuminance;
mode = Av1ColorConversionMode.ConstantLuminance;
GetChromaticityDerivedCoefficients(frameBuffer.ColorConfig.ColorPrimaries, out kr, out kb);
break;
case ObuMatrixCoefficients.Bt2100ICtCp:
mode = ConversionMode.ICtCp;
mode = Av1ColorConversionMode.ICtCp;
break;
default:
throw new NotSupportedException($"AV1 matrix coefficients '{frameBuffer.ColorConfig.MatrixCoefficients}' are not currently supported.");
@ -374,7 +239,7 @@ internal static partial class Av1YuvConverter
kg = 1F - kr - kb;
bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
bool isFullRange = frameBuffer.ColorConfig.ColorRange;
if (mode == ConversionMode.Identity && !isMonochrome && frameBuffer.ColorFormat != Av1ColorFormat.Yuv444)
if (mode == Av1ColorConversionMode.Identity && !isMonochrome && frameBuffer.ColorFormat != Av1ColorFormat.Yuv444)
{
throw new InvalidImageContentException("AV1 identity matrix coefficients require YUV 4:4:4 sampling.");
}
@ -393,7 +258,7 @@ internal static partial class Av1YuvConverter
// H.273 limited-range YCgCo first maps R, G, and B through the 219-code luma range, so its
// difference components inherit that scale instead of the 224-code scale used by YCbCr.
chromaScale = isFullRange || mode == ConversionMode.YCgCo
chromaScale = isFullRange || mode == Av1ColorConversionMode.YCgCo
? lumaScale
: 224F * depthScale;
}
@ -548,227 +413,6 @@ internal static partial class Av1YuvConverter
(whiteZ * ((redX * greenY) - (greenX * redY))))) / denominator;
}
/// <summary>
/// Converts one YUV row to packed RGB using the resolved H.273 conversion state.
/// </summary>
/// <typeparam name="TSample">The encoded sample type.</typeparam>
/// <typeparam name="TRgb">The packed RGB staging type.</typeparam>
/// <param name="ySource">The luma samples.</param>
/// <param name="uRow0">The upper blue-difference chroma row.</param>
/// <param name="uRow1">The lower blue-difference chroma row.</param>
/// <param name="vRow0">The upper red-difference chroma row.</param>
/// <param name="vRow1">The lower red-difference chroma row.</param>
/// <param name="y1Weight">The lower chroma-row weight with a denominator of four.</param>
/// <param name="destination">The destination RGB pixels.</param>
/// <param name="isMonochrome">Whether the frame contains only luma samples.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="chromaSamplePosition">The spatial position of subsampled chroma.</param>
/// <param name="mode">The conversion mode.</param>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kg">The green luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="constantLuminanceScales">The constant-luminance chroma scales.</param>
/// <param name="lumaBias">The encoded luma bias.</param>
/// <param name="lumaScale">The encoded luma range.</param>
/// <param name="chromaBias">The encoded chroma midpoint.</param>
/// <param name="chromaScale">The encoded chroma range.</param>
/// <param name="sampleMaximum">The largest encoded sample value.</param>
private static void ConvertYuvToRgbRow<TSample, TRgb>(
ReadOnlySpan<TSample> ySource,
ReadOnlySpan<TSample> uRow0,
ReadOnlySpan<TSample> uRow1,
ReadOnlySpan<TSample> vRow0,
ReadOnlySpan<TSample> vRow1,
int y1Weight,
Span<TRgb> destination,
bool isMonochrome,
int subX,
int subY,
ObuChromoSamplePosition chromaSamplePosition,
ConversionMode mode,
float kr,
float kg,
float kb,
ObuTransferCharacteristics transferCharacteristics,
in ConstantLuminanceScales constantLuminanceScales,
float lumaBias,
float lumaScale,
float chromaBias,
float chromaScale,
float sampleMaximum)
where TSample : unmanaged
where TRgb : unmanaged
{
for (int x = 0; x < destination.Length; x++)
{
float y = (GetSample(ySource, x) - lumaBias) / lumaScale;
float r;
float g;
float b;
if (isMonochrome)
{
r = y;
g = y;
b = y;
}
else
{
float u = SampleChroma(uRow0, uRow1, x, subX, subY, chromaSamplePosition, y1Weight);
float v = SampleChroma(vRow0, vRow1, x, subX, subY, chromaSamplePosition, y1Weight);
float cb = (u - chromaBias) / chromaScale;
float cr = (v - chromaBias) / chromaScale;
switch (mode)
{
case ConversionMode.Identity:
// H.273 identity coding stores the nonlinear G, B, and R signals in Y, U, and V order.
r = (v - lumaBias) / lumaScale;
g = y;
b = (u - lumaBias) / lumaScale;
break;
case ConversionMode.YCgCo:
float temporary = y - cb;
r = temporary + cr;
g = y + cb;
b = temporary - cr;
break;
case ConversionMode.Smpte2085:
// H.273 equations 76 to 78 store green as luma and use the ST 2085 scale factors for
// the blue and red difference components.
g = y;
b = ((2F * cb) + y) / 0.986566F;
r = (2F * cr) + (0.991902F * y);
break;
case ConversionMode.ConstantLuminance:
// H.273 equations 66 to 75 define luma in linear light, while the stored luma and
// difference signals remain nonlinear. Reconstruct red and blue before solving green.
float nonlinearBlue = y +
(2F * (cb <= 0F ? constantLuminanceScales.NegativeBlue : constantLuminanceScales.PositiveBlue) * cb);
float nonlinearRed = y +
(2F * (cr <= 0F ? constantLuminanceScales.NegativeRed : constantLuminanceScales.PositiveRed) * cr);
float linearY = Av1TransferFunctions.ToLinear(transferCharacteristics, y);
float linearBlue = Av1TransferFunctions.ToLinear(transferCharacteristics, nonlinearBlue);
float linearRed = Av1TransferFunctions.ToLinear(transferCharacteristics, nonlinearRed);
float linearGreen = (linearY - (kr * linearRed) - (kb * linearBlue)) / kg;
r = nonlinearRed;
g = Av1TransferFunctions.ToGamma(transferCharacteristics, linearGreen);
b = nonlinearBlue;
break;
case ConversionMode.ICtCp:
float nonlinearL;
float nonlinearM;
float nonlinearS;
if (transferCharacteristics == ObuTransferCharacteristics.Hlg)
{
// This is the exact inverse of H.273 equations 82 to 84. The first column is one
// because intensity is defined as the average of the L and M components.
nonlinearL = y + (0.015718580108730413F * cb) + (0.2095810681164055F * cr);
nonlinearM = y - (0.015718580108730413F * cb) - (0.2095810681164055F * cr);
nonlinearS = y + (1.0212710798422342F * cb) - (0.6052744909924315F * cr);
}
else
{
// H.273 equations 79 to 81 are the ICtCp matrix selected for PQ and every transfer
// code other than HLG. These constants are the exact inverse of its integer matrix.
nonlinearL = y + (0.008609037037932756F * cb) + (0.11102962500302596F * cr);
nonlinearM = y - (0.008609037037932756F * cb) - (0.11102962500302596F * cr);
nonlinearS = y + (0.5600313357106791F * cb) - (0.32062717498731885F * cr);
}
float linearL = Av1TransferFunctions.ToLinear(transferCharacteristics, nonlinearL);
float linearM = Av1TransferFunctions.ToLinear(transferCharacteristics, nonlinearM);
float linearS = Av1TransferFunctions.ToLinear(transferCharacteristics, nonlinearS);
// This cofactor inverse of H.273 equations 14 to 16 recovers linear RGB from LMS.
// Applying the transfer curve last returns the nonlinear RGB values stored by ImageSharp.
float ictcpLinearRed =
(3.4366066943330784F * linearL) -
(2.50645211865627F * linearM) +
(0.06984542432319148F * linearS);
float ictcpLinearGreen =
(-0.7913295555989287F * linearL) +
(1.9836004517922907F * linearM) -
(0.192270896193362F * linearS);
float ictcpLinearBlue =
(-0.025949899690592672F * linearL) -
(0.09891371471172644F * linearM) +
(1.1248636144023192F * linearS);
r = Av1TransferFunctions.ToGamma(transferCharacteristics, ictcpLinearRed);
g = Av1TransferFunctions.ToGamma(transferCharacteristics, ictcpLinearGreen);
b = Av1TransferFunctions.ToGamma(transferCharacteristics, ictcpLinearBlue);
break;
default:
r = y + (2F * (1F - kr) * cr);
g = y - (2F * ((kr * (1F - kr) * cr) + (kb * (1F - kb) * cb)) / kg);
b = y + (2F * (1F - kb) * cb);
break;
}
}
// The generic staging type is controlled by the frame bit depth. The JIT removes the inactive branch,
// retaining direct component access without routing every pixel through Vector4 or interface dispatch.
if (typeof(TRgb) == typeof(Rgb24))
{
Rgb24 pixel = new(
ToSample<byte>(r * ByteMaximum, ByteMaximum),
ToSample<byte>(g * ByteMaximum, ByteMaximum),
ToSample<byte>(b * ByteMaximum, ByteMaximum));
destination[x] = Unsafe.As<Rgb24, TRgb>(ref pixel);
}
else
{
Rgb48 pixel = new(
ToSample<ushort>(r * UShortMaximum, UShortMaximum),
ToSample<ushort>(g * UShortMaximum, UShortMaximum),
ToSample<ushort>(b * UShortMaximum, UShortMaximum));
destination[x] = Unsafe.As<Rgb48, TRgb>(ref pixel);
}
}
}
/// <summary>
/// Bilinearly reconstructs a chroma sample at a luma coordinate.
/// </summary>
/// <typeparam name="TSample">The encoded sample type.</typeparam>
/// <param name="row0">The upper chroma row.</param>
/// <param name="row1">The lower chroma row.</param>
/// <param name="x">The luma column coordinate.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <param name="chromaSamplePosition">The spatial position of subsampled chroma.</param>
/// <param name="y1Weight">The lower chroma-row weight with a denominator of four.</param>
/// <returns>The reconstructed encoded chroma sample.</returns>
private static float SampleChroma<TSample>(
ReadOnlySpan<TSample> row0,
ReadOnlySpan<TSample> row1,
int x,
int subX,
int subY,
ObuChromoSamplePosition chromaSamplePosition,
int y1Weight)
where TSample : unmanaged
{
// Unknown 4:2:0 and all 4:2:2 input use the centered convention employed by libavif.
bool isCenteredX = subX != 0 && (subY == 0 || chromaSamplePosition == ObuChromoSamplePosition.Unknown);
GetChromaCoordinates(x, subX, isCenteredX, row0.Length - 1, out int x0, out int x1, out int x1Weight);
float top = (GetSample(row0, x0) * (4 - x1Weight)) + (GetSample(row0, x1) * x1Weight);
float bottom = (GetSample(row1, x0) * (4 - x1Weight)) + (GetSample(row1, x1) * x1Weight);
return ((top * (4 - y1Weight)) + (bottom * y1Weight)) / 16F;
}
/// <summary>
/// Resolves the two chroma samples and quarter-sample weight surrounding a luma coordinate.
/// </summary>
@ -811,287 +455,6 @@ internal static partial class Av1YuvConverter
upperWeight = isOdd ? 2 : 0;
}
/// <summary>
/// Converts one packed RGB row to luma and optional full-resolution chroma using the resolved H.273 conversion state.
/// </summary>
/// <typeparam name="TSample">The encoded sample type.</typeparam>
/// <typeparam name="TRgb">The packed RGB staging type.</typeparam>
/// <param name="source">The source RGB pixels.</param>
/// <param name="yDestination">The destination luma samples.</param>
/// <param name="uDestination">The destination blue-difference chroma samples.</param>
/// <param name="vDestination">The destination red-difference chroma samples.</param>
/// <param name="mode">The conversion mode.</param>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kg">The green luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="constantLuminanceScales">The constant-luminance chroma scales.</param>
/// <param name="lumaBias">The encoded luma bias.</param>
/// <param name="lumaScale">The encoded luma range.</param>
/// <param name="chromaBias">The encoded chroma midpoint.</param>
/// <param name="chromaScale">The encoded chroma range.</param>
/// <param name="sampleMaximum">The largest encoded sample value.</param>
private static void ConvertRgbToYuvRow<TSample, TRgb>(
ReadOnlySpan<TRgb> source,
Span<TSample> yDestination,
Span<TSample> uDestination,
Span<TSample> vDestination,
ConversionMode mode,
float kr,
float kg,
float kb,
ObuTransferCharacteristics transferCharacteristics,
in ConstantLuminanceScales constantLuminanceScales,
float lumaBias,
float lumaScale,
float chromaBias,
float chromaScale,
float sampleMaximum)
where TSample : unmanaged
where TRgb : unmanaged
{
for (int x = 0; x < source.Length; x++)
{
ConvertRgbToYuv(
source[x],
mode,
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
out float y,
out float cb,
out float cr);
yDestination[x] = ToSample<TSample>((y * lumaScale) + lumaBias, sampleMaximum);
if (!uDestination.IsEmpty)
{
if (mode == ConversionMode.Identity)
{
uDestination[x] = ToSample<TSample>((cb * lumaScale) + lumaBias, sampleMaximum);
vDestination[x] = ToSample<TSample>((cr * lumaScale) + lumaBias, sampleMaximum);
}
else
{
uDestination[x] = ToSample<TSample>((cb * chromaScale) + chromaBias, sampleMaximum);
vDestination[x] = ToSample<TSample>((cr * chromaScale) + chromaBias, sampleMaximum);
}
}
}
}
/// <summary>
/// Converts one or two packed RGB rows to luma and horizontally subsampled chroma.
/// </summary>
/// <typeparam name="TSample">The encoded sample type.</typeparam>
/// <typeparam name="TRgb">The packed RGB staging type.</typeparam>
/// <param name="sourceRow0">The first source row.</param>
/// <param name="sourceRow1">The optional second source row for 4:2:0 conversion.</param>
/// <param name="yDestination0">The first destination luma row.</param>
/// <param name="yDestination1">The optional second destination luma row.</param>
/// <param name="uDestination">The destination blue-difference chroma row.</param>
/// <param name="vDestination">The destination red-difference chroma row.</param>
/// <param name="mode">The conversion mode.</param>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kg">The green luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="constantLuminanceScales">The constant-luminance chroma scales.</param>
/// <param name="lumaBias">The encoded luma bias.</param>
/// <param name="lumaScale">The encoded luma range.</param>
/// <param name="chromaBias">The encoded chroma midpoint.</param>
/// <param name="chromaScale">The encoded chroma range.</param>
/// <param name="sampleMaximum">The largest encoded sample value.</param>
private static void ConvertRgbToSubsampledYuvRows<TSample, TRgb>(
ReadOnlySpan<TRgb> sourceRow0,
ReadOnlySpan<TRgb> sourceRow1,
Span<TSample> yDestination0,
Span<TSample> yDestination1,
Span<TSample> uDestination,
Span<TSample> vDestination,
ConversionMode mode,
float kr,
float kg,
float kb,
ObuTransferCharacteristics transferCharacteristics,
in ConstantLuminanceScales constantLuminanceScales,
float lumaBias,
float lumaScale,
float chromaBias,
float chromaScale,
float sampleMaximum)
where TSample : unmanaged
where TRgb : unmanaged
{
int rowCount = sourceRow1.IsEmpty ? 1 : 2;
for (int x = 0; x < sourceRow0.Length; x += 2)
{
int columnCount = Math.Min(2, sourceRow0.Length - x);
float cbSum = 0F;
float crSum = 0F;
for (int row = 0; row < rowCount; row++)
{
ReadOnlySpan<TRgb> source = row == 0 ? sourceRow0 : sourceRow1;
Span<TSample> yDestination = row == 0 ? yDestination0 : yDestination1;
for (int column = 0; column < columnCount; column++)
{
int sourceIndex = x + column;
ConvertRgbToYuv(
source[sourceIndex],
mode,
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
out float y,
out float cb,
out float cr);
yDestination[sourceIndex] = ToSample<TSample>((y * lumaScale) + lumaBias, sampleMaximum);
cbSum += cb;
crSum += cr;
}
}
// libavif's scalar average path divides by the actual edge-block dimensions, so odd widths and heights
// do not replicate a missing RGB sample into the chroma average.
float sampleCount = columnCount * rowCount;
float cbAverage = cbSum / sampleCount;
float crAverage = crSum / sampleCount;
int chromaIndex = x >> 1;
uDestination[chromaIndex] = ToSample<TSample>((cbAverage * chromaScale) + chromaBias, sampleMaximum);
vDestination[chromaIndex] = ToSample<TSample>((crAverage * chromaScale) + chromaBias, sampleMaximum);
}
}
/// <summary>
/// Converts one packed RGB pixel to normalized luma and chroma values.
/// </summary>
/// <typeparam name="TRgb">The packed RGB staging type.</typeparam>
/// <param name="pixel">The source RGB pixel.</param>
/// <param name="mode">The conversion mode.</param>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kg">The green luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="constantLuminanceScales">The constant-luminance chroma scales.</param>
/// <param name="y">The normalized luma result.</param>
/// <param name="cb">The normalized blue-difference chroma result.</param>
/// <param name="cr">The normalized red-difference chroma result.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ConvertRgbToYuv<TRgb>(
TRgb pixel,
ConversionMode mode,
float kr,
float kg,
float kb,
ObuTransferCharacteristics transferCharacteristics,
in ConstantLuminanceScales constantLuminanceScales,
out float y,
out float cb,
out float cr)
where TRgb : unmanaged
{
float r;
float g;
float b;
// These are the only staging formats selected by the owning conversion methods. Keeping the format choice
// generic lets the JIT specialize the hot loop and preserves high-bit-depth input without boxing or copies.
if (typeof(TRgb) == typeof(Rgb24))
{
Rgb24 rgb24 = Unsafe.As<TRgb, Rgb24>(ref pixel);
r = rgb24.R / ByteMaximum;
g = rgb24.G / ByteMaximum;
b = rgb24.B / ByteMaximum;
}
else
{
Rgb48 rgb48 = Unsafe.As<TRgb, Rgb48>(ref pixel);
r = rgb48.R / UShortMaximum;
g = rgb48.G / UShortMaximum;
b = rgb48.B / UShortMaximum;
}
switch (mode)
{
case ConversionMode.Identity:
// H.273 identity coding stores the nonlinear G, B, and R signals in Y, U, and V order.
y = g;
cb = b;
cr = r;
break;
case ConversionMode.YCgCo:
y = (0.5F * g) + (0.25F * (r + b));
cb = (0.5F * g) - (0.25F * (r + b));
cr = 0.5F * (r - b);
break;
case ConversionMode.Smpte2085:
// ST 2085 uses green directly as luma, so this path must remain separate from Kr/Kb YCbCr.
y = g;
cb = ((0.986566F * b) - y) * 0.5F;
cr = (r - (0.991902F * y)) * 0.5F;
break;
case ConversionMode.ConstantLuminance:
// The packed RGB values are nonlinear signal components. H.273 constant luminance derives Y
// after applying the inverse transfer curve to each component.
float linearRed = Av1TransferFunctions.ToLinear(transferCharacteristics, r);
float linearGreen = Av1TransferFunctions.ToLinear(transferCharacteristics, g);
float linearBlue = Av1TransferFunctions.ToLinear(transferCharacteristics, b);
float linearY = (kr * linearRed) + (kg * linearGreen) + (kb * linearBlue);
y = Av1TransferFunctions.ToGamma(transferCharacteristics, linearY);
float blueDifference = b - y;
float redDifference = r - y;
cb = blueDifference /
(2F * (blueDifference <= 0F ? constantLuminanceScales.NegativeBlue : constantLuminanceScales.PositiveBlue));
cr = redDifference /
(2F * (redDifference <= 0F ? constantLuminanceScales.NegativeRed : constantLuminanceScales.PositiveRed));
break;
case ConversionMode.ICtCp:
float ictcpLinearRed = Av1TransferFunctions.ToLinear(transferCharacteristics, r);
float ictcpLinearGreen = Av1TransferFunctions.ToLinear(transferCharacteristics, g);
float ictcpLinearBlue = Av1TransferFunctions.ToLinear(transferCharacteristics, b);
// H.273 equations 14 to 16 convert linear BT.2100 RGB into the LMS cone-response domain
// before the signaled transfer curve is applied to each component.
float nonlinearL = Av1TransferFunctions.ToGamma(
transferCharacteristics,
((1688F * ictcpLinearRed) + (2146F * ictcpLinearGreen) + (262F * ictcpLinearBlue)) / 4096F);
float nonlinearM = Av1TransferFunctions.ToGamma(
transferCharacteristics,
((683F * ictcpLinearRed) + (2951F * ictcpLinearGreen) + (462F * ictcpLinearBlue)) / 4096F);
float nonlinearS = Av1TransferFunctions.ToGamma(
transferCharacteristics,
((99F * ictcpLinearRed) + (309F * ictcpLinearGreen) + (3688F * ictcpLinearBlue)) / 4096F);
y = 0.5F * (nonlinearL + nonlinearM);
if (transferCharacteristics == ObuTransferCharacteristics.Hlg)
{
cb = ((3625F * nonlinearL) - (7465F * nonlinearM) + (3840F * nonlinearS)) / 4096F;
cr = ((9500F * nonlinearL) - (9212F * nonlinearM) - (288F * nonlinearS)) / 4096F;
}
else
{
cb = ((6610F * nonlinearL) - (13613F * nonlinearM) + (7003F * nonlinearS)) / 4096F;
cr = ((17933F * nonlinearL) - (17390F * nonlinearM) - (543F * nonlinearS)) / 4096F;
}
break;
default:
y = (kr * r) + (kg * g) + (kb * b);
cb = (b - y) / (2F * (1F - kb));
cr = (r - y) / (2F * (1F - kr));
break;
}
}
/// <summary>
/// Reads an 8-bit or 16-bit unsigned sample without introducing a separate conversion buffer.
/// </summary>
@ -1130,47 +493,4 @@ internal static partial class Av1YuvConverter
ushort highBitDepthResult = (ushort)sample;
return Unsafe.As<ushort, TSample>(ref highBitDepthResult);
}
/// <summary>
/// Stores the H.273 chroma normalization constants for constant-luminance conversion.
/// </summary>
private readonly struct ConstantLuminanceScales
{
/// <summary>
/// Initializes a new instance of the <see cref="ConstantLuminanceScales"/> struct.
/// </summary>
/// <param name="transferCharacteristics">The signaled transfer characteristics.</param>
/// <param name="kr">The red luma coefficient.</param>
/// <param name="kb">The blue luma coefficient.</param>
public ConstantLuminanceScales(
ObuTransferCharacteristics transferCharacteristics,
float kr,
float kb)
{
this.NegativeBlue = Av1TransferFunctions.ToGamma(transferCharacteristics, 1F - kb);
this.PositiveBlue = 1F - Av1TransferFunctions.ToGamma(transferCharacteristics, kb);
this.NegativeRed = Av1TransferFunctions.ToGamma(transferCharacteristics, 1F - kr);
this.PositiveRed = 1F - Av1TransferFunctions.ToGamma(transferCharacteristics, kr);
}
/// <summary>
/// Gets the scale for a non-positive blue difference.
/// </summary>
public float NegativeBlue { get; }
/// <summary>
/// Gets the scale for a positive blue difference.
/// </summary>
public float PositiveBlue { get; }
/// <summary>
/// Gets the scale for a non-positive red difference.
/// </summary>
public float NegativeRed { get; }
/// <summary>
/// Gets the scale for a positive red difference.
/// </summary>
public float PositiveRed { get; }
}
}

129
tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs

@ -0,0 +1,129 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif;
/// <summary>
/// Measures frame-wide AV1 YUV 4:2:0 color conversion in both directions.
/// </summary>
[MemoryDiagnoser(displayGenColumns: false)]
public class Av1ColorConversionBenchmarks
{
/// <summary>
/// The benchmark frame width.
/// </summary>
private const int Width = 1920;
/// <summary>
/// The benchmark frame height.
/// </summary>
private const int Height = 1080;
/// <summary>
/// The source RGB image.
/// </summary>
private Image<Rgb48> source = null!;
/// <summary>
/// The destination RGB image.
/// </summary>
private Image<Rgb48> destination = null!;
/// <summary>
/// The reusable AV1 frame planes.
/// </summary>
private Av1FrameBuffer<byte> frameBuffer = null!;
/// <summary>
/// Gets or sets the encoded AV1 bit depth.
/// </summary>
[Params(8, 10, 12)]
public int BitDepth { get; set; }
/// <summary>
/// Allocates and populates deterministic full-HD RGB and YUV frames outside the measured operations.
/// </summary>
[GlobalSetup]
public void Setup()
{
this.source = new Image<Rgb48>(Width, Height);
this.destination = new Image<Rgb48>(Width, Height);
for (int y = 0; y < Height; y++)
{
Span<Rgb48> row = this.source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < Width; x++)
{
// The relatively prime channel steps avoid uniform rows while remaining deterministic.
row[x] = new Rgb48(
(ushort)((x * 1879) + (y * 791)),
(ushort)((x * 977) + (y * 3251)),
(ushort)((x * 613) + (y * 4987)));
}
}
ObuSequenceHeader sequenceHeader = new()
{
MaxFrameWidth = Width,
MaxFrameHeight = Height,
ColorConfig = new ObuColorConfig
{
BitDepth = this.BitDepth switch
{
10 => Av1BitDepth.TenBit,
12 => Av1BitDepth.TwelveBit,
_ => Av1BitDepth.EightBit,
},
ColorPrimaries = ObuColorPrimaries.Bt709,
TransferCharacteristics = ObuTransferCharacteristics.Bt709,
MatrixCoefficients = ObuMatrixCoefficients.Bt709,
ColorRange = false,
SubSamplingX = true,
SubSamplingY = true,
ChromaSamplePosition = ObuChromoSamplePosition.Unknown,
},
};
this.frameBuffer = new Av1FrameBuffer<byte>(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false);
Av1YuvConverter.ConvertFromRgb(Configuration.Default, this.source.Frames.RootFrame, this.frameBuffer);
}
/// <summary>
/// Releases the benchmark images and reconstructed planes.
/// </summary>
[GlobalCleanup]
public void Cleanup()
{
this.frameBuffer.Dispose();
this.destination.Dispose();
this.source.Dispose();
}
/// <summary>
/// Measures full-frame YUV-to-RGB conversion, including chroma reconstruction and packed-pixel conversion.
/// </summary>
/// <returns>A converted pixel that keeps the frame result observable.</returns>
[Benchmark]
public Rgb48 ConvertToRgb()
{
Av1YuvConverter.ConvertToRgb(Configuration.Default, this.frameBuffer, this.destination.Frames.RootFrame);
return this.destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(Height - 1)[Width - 1];
}
/// <summary>
/// Measures full-frame RGB-to-YUV conversion, including planar unpacking and chroma downsampling.
/// </summary>
/// <returns>An encoded luma sample that keeps the frame result observable.</returns>
[Benchmark]
public int ConvertFromRgb()
{
Av1YuvConverter.ConvertFromRgb(Configuration.Default, this.source.Frames.RootFrame, this.frameBuffer);
return this.BitDepth == 8
? this.frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(Height - 1)[Width - 1]
: this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, Height - 1, 0, 0)[Width - 1];
}
}

319
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs

@ -10,9 +10,23 @@ using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1;
/// <summary>
/// Verifies AV1 color conversion, sample-range handling, chroma reconstruction, and encoder downsampling.
/// </summary>
[Trait("Format", "Avif")]
public class Av1YuvConverterTests
{
/// <summary>
/// Verifies known RGB-to-YUV values across coefficient, identity, and YCgCo matrices and sample ranges.
/// </summary>
/// <param name="r">The source red component.</param>
/// <param name="g">The source green component.</param>
/// <param name="b">The source blue component.</param>
/// <param name="y">The expected luma or first encoded component.</param>
/// <param name="u">The expected first chroma or second encoded component.</param>
/// <param name="v">The expected second chroma or third encoded component.</param>
/// <param name="fullRange">Whether the encoded samples use the full range.</param>
/// <param name="matrixCoefficients">The matrix coefficients used for conversion.</param>
[Theory]
[InlineData(255, 255, 255, 255, 128, 128, true, ObuMatrixCoefficients.Bt709)]
[InlineData(0, 0, 0, 0, 128, 128, true, ObuMatrixCoefficients.Bt709)]
@ -52,6 +66,17 @@ public class Av1YuvConverterTests
Assert.Equal(v, actualV);
}
/// <summary>
/// Verifies known YUV-to-RGB values across coefficient, identity, and YCgCo matrices and sample ranges.
/// </summary>
/// <param name="r">The expected red component.</param>
/// <param name="g">The expected green component.</param>
/// <param name="b">The expected blue component.</param>
/// <param name="y">The source luma or first encoded component.</param>
/// <param name="u">The source first chroma or second encoded component.</param>
/// <param name="v">The source second chroma or third encoded component.</param>
/// <param name="fullRange">Whether the encoded samples use the full range.</param>
/// <param name="matrixCoefficients">The matrix coefficients used for conversion.</param>
[Theory]
[InlineData(255, 255, 255, 255, 128, 128, true, ObuMatrixCoefficients.Bt709)]
[InlineData(0, 0, 0, 0, 128, 128, true, ObuMatrixCoefficients.Bt709)]
@ -91,6 +116,9 @@ public class Av1YuvConverterTests
Assert.Equal(b, actual.B, 1d);
}
/// <summary>
/// Verifies that limited-range monochrome samples expand to the complete RGB output range.
/// </summary>
[Fact]
public void Yuv400ToRgbExpandsLimitedRangeLuma()
{
@ -117,6 +145,48 @@ public class Av1YuvConverterTests
Assert.Equal(new Rgb24(255, 255, 255), actual[1]);
}
/// <summary>
/// Verifies RGB-to-monochrome conversion and range quantization for every supported AV1 bit depth.
/// </summary>
/// <param name="bitDepth">The encoded AV1 bit depth.</param>
/// <param name="fullRange">Whether the luma samples use the full range.</param>
/// <param name="expectedLuma">The expected encoded luma sample.</param>
[Theory]
[InlineData(Av1BitDepth.EightBit, true, 107)]
[InlineData(Av1BitDepth.EightBit, false, 108)]
[InlineData(Av1BitDepth.TenBit, true, 429)]
[InlineData(Av1BitDepth.TenBit, false, 432)]
[InlineData(Av1BitDepth.TwelveBit, true, 1719)]
[InlineData(Av1BitDepth.TwelveBit, false, 1727)]
public void RgbToYuv400WritesQuantizedLuma(int bitDepth, bool fullRange, int expectedLuma)
{
// Rgb48 values scaled from eight-bit components exercise the precision-preserving high-bit-depth path.
using Image<Rgb48> image = new(1, 1);
image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb48(150 * 257, 100 * 257, 50 * 257);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(
1,
1,
fullRange,
colorFormat: Av1ColorFormat.Yuv400,
bitDepth: (Av1BitDepth)bitDepth);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false);
Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer);
Assert.Equal(expectedLuma, GetPlaneSample(frameBuffer, Av1Plane.Y, 0, 0, 0, 0));
Assert.Null(frameBuffer.BufferCb);
Assert.Null(frameBuffer.BufferCr);
}
/// <summary>
/// Verifies full- and limited-range expansion for 10-bit and 12-bit reconstructed samples.
/// </summary>
/// <param name="bitDepth">The reconstructed AV1 bit depth.</param>
/// <param name="fullRange">Whether the samples use the full range.</param>
/// <param name="black">The encoded black luma sample.</param>
/// <param name="white">The encoded white luma sample.</param>
/// <param name="neutralChroma">The neutral encoded chroma sample.</param>
[Theory]
[InlineData(Av1BitDepth.TenBit, true, 0, 1023, 512)]
[InlineData(Av1BitDepth.TenBit, false, 64, 940, 512)]
@ -148,6 +218,10 @@ public class Av1YuvConverterTests
Assert.Equal(new Rgb24(255, 255, 255), actual[1]);
}
/// <summary>
/// Verifies that high-bit-depth frame strides and row access use 16-bit sample units consistently.
/// </summary>
/// <param name="bitDepth">The reconstructed AV1 bit depth.</param>
[Theory]
[InlineData(Av1BitDepth.TenBit)]
[InlineData(Av1BitDepth.TwelveBit)]
@ -175,6 +249,9 @@ public class Av1YuvConverterTests
Assert.Equal(2, chromaRow.Length);
}
/// <summary>
/// Verifies centered horizontal chroma reconstruction for a YUV 4:2:2 frame.
/// </summary>
[Fact]
public void Yuv422ToRgbBilinearlyUpsamplesCenteredChroma()
{
@ -199,6 +276,12 @@ public class Av1YuvConverterTests
Assert.Equal(new Rgb24(128, 116, 247), actual[3]);
}
/// <summary>
/// Verifies vertical and horizontal YUV 4:2:0 reconstruction at every AV1 chroma sample position.
/// </summary>
/// <param name="chromaSamplePosition">The signaled AV1 chroma sample position.</param>
/// <param name="expectedTopBlue">The expected blue component in the top-row probe pixel.</param>
/// <param name="expectedLeftBlue">The expected blue component in the left-column probe pixel.</param>
[Theory]
[InlineData(ObuChromoSamplePosition.Unknown, 158, 98)]
[InlineData(ObuChromoSamplePosition.Vertical, 187, 98)]
@ -237,6 +320,9 @@ public class Av1YuvConverterTests
Assert.Equal(expectedLeftBlue, image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(1)[0].B);
}
/// <summary>
/// Verifies that odd image dimensions retain the final YUV 4:2:0 chroma row and column.
/// </summary>
[Fact]
public void Yuv420UsesCeilingChromaPlaneDimensions()
{
@ -253,6 +339,9 @@ public class Av1YuvConverterTests
Assert.Equal(new Size(2, 2), vPlane.Size);
}
/// <summary>
/// Compares SIMD-first RGB-to-YUV conversion with the independent scalar reference over randomized pixels.
/// </summary>
[Fact]
public void RgbToYuvCompareToReferenceRandomPixels()
{
@ -290,6 +379,9 @@ public class Av1YuvConverterTests
Compare(referenceOutput, actual, 3);
}
/// <summary>
/// Compares SIMD-first YUV-to-RGB conversion with the independent scalar reference over randomized samples.
/// </summary>
[Fact]
public void YuvToRgbCompareToReferenceRandomPixels()
{
@ -315,6 +407,12 @@ public class Av1YuvConverterTests
Compare(referenceOutput, actual, 3);
}
/// <summary>
/// Compares packed RGB rows within the permitted per-component tolerance.
/// </summary>
/// <param name="referenceOutput">The independently converted reference pixels.</param>
/// <param name="actual">The pixels produced by the implementation under test.</param>
/// <param name="allowedDifference">The permitted absolute component difference.</param>
private static void Compare(Span<Rgb24> referenceOutput, Span<Rgb24> actual, int allowedDifference)
{
for (int i = 0; i < actual.Length; i++)
@ -328,6 +426,12 @@ public class Av1YuvConverterTests
}
}
/// <summary>
/// Fills one reconstructed plane with deterministic pseudo-random test samples.
/// </summary>
/// <param name="rnd">The deterministic random number generator.</param>
/// <param name="frameBuffer">The frame containing the destination plane.</param>
/// <param name="plane">The destination plane.</param>
private static void CreateTestData(Random rnd, Av1FrameBuffer<byte> frameBuffer, Av1Plane plane)
{
const int bitCount = 8;
@ -338,6 +442,12 @@ public class Av1YuvConverterTests
}
}
/// <summary>
/// Fills an eight-bit sample span with deterministic pseudo-random values.
/// </summary>
/// <param name="rnd">The deterministic random number generator.</param>
/// <param name="span">The destination sample span.</param>
/// <param name="bitCount">The number of significant sample bits.</param>
private static void CreateTestData(Random rnd, Span<byte> span, int bitCount = 8)
{
int max = (1 << bitCount) - 1;
@ -348,6 +458,12 @@ public class Av1YuvConverterTests
}
}
/// <summary>
/// Fills a high-bit-depth sample span with deterministic pseudo-random values.
/// </summary>
/// <param name="rnd">The deterministic random number generator.</param>
/// <param name="span">The destination sample span.</param>
/// <param name="bitCount">The number of significant sample bits.</param>
private static void CreateTestData(Random rnd, Span<ushort> span, int bitCount)
{
int max = (1 << bitCount) - 1;
@ -358,6 +474,12 @@ public class Av1YuvConverterTests
}
}
/// <summary>
/// Verifies RGB-to-YUV-to-RGB conversion for representative single-pixel colors.
/// </summary>
/// <param name="r">The source red component.</param>
/// <param name="g">The source green component.</param>
/// <param name="b">The source blue component.</param>
[Theory]
[InlineData(255, 255, 255)]
[InlineData(0, 0, 0)]
@ -391,6 +513,11 @@ public class Av1YuvConverterTests
Assert.Equal(b, actualPixel.B, 2d);
}
/// <summary>
/// Verifies 10-bit and 12-bit round trips for coefficient, identity, and YCgCo matrices.
/// </summary>
/// <param name="bitDepth">The encoded AV1 bit depth.</param>
/// <param name="matrixCoefficients">The matrix coefficients used for conversion.</param>
[Theory]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Bt709)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Identity)]
@ -429,6 +556,108 @@ public class Av1YuvConverterTests
}
}
/// <summary>
/// Verifies that every H.273 operator produces the same result in SIMD batches and the scalar row tail.
/// </summary>
/// <param name="matrixCoefficients">The matrix coefficients selecting the color operator.</param>
/// <param name="transferCharacteristics">The transfer characteristics used by nonlinear operators.</param>
[Theory]
[InlineData(ObuMatrixCoefficients.Bt709, ObuTransferCharacteristics.Bt709)]
[InlineData(ObuMatrixCoefficients.Identity, ObuTransferCharacteristics.Bt709)]
[InlineData(ObuMatrixCoefficients.SmpteYCgCo, ObuTransferCharacteristics.Bt709)]
[InlineData(ObuMatrixCoefficients.Bt2020ConstantLuminance, ObuTransferCharacteristics.Bt202010Bit)]
[InlineData(ObuMatrixCoefficients.Smpte2085, ObuTransferCharacteristics.Bt709)]
[InlineData(ObuMatrixCoefficients.ChromaticityDerivedNonConstantLuminance, ObuTransferCharacteristics.Bt709)]
[InlineData(ObuMatrixCoefficients.ChromaticityDerivedConstantLuminance, ObuTransferCharacteristics.Bt709)]
[InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Smpte2084)]
[InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Hlg)]
public void ColorOperatorSimdBatchesMatchScalarTail(int matrixCoefficients, int transferCharacteristics)
{
const int width = 31;
// Thirty-one samples exercise Vector512, Vector256, Vector128, and scalar stages on AVX-512 hardware.
// The same row still reaches the widest available stages and scalar tail on narrower SIMD hardware.
using Image<Rgb48> source = new(width, 1);
source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0).Fill(new Rgb48(39999, 27777, 12345));
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(
width,
1,
matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients,
bitDepth: Av1BitDepth.TwelveBit,
transferCharacteristics: (ObuTransferCharacteristics)transferCharacteristics,
colorPrimaries: ObuColorPrimaries.Bt2020);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
using Image<Rgb48> destination = new(width, 1);
Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame);
AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.Y, 0, 0);
AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.U, 0, 0);
AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.V, 0, 0);
Span<Rgb48> pixels = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
for (int x = 1; x < pixels.Length; x++)
{
Assert.Equal(pixels[0], pixels[x]);
}
}
/// <summary>
/// Verifies horizontal and vertical chroma downsampling against full-resolution encoded components.
/// </summary>
/// <param name="colorFormat">The subsampled AV1 color format.</param>
/// <param name="bitDepth">The encoded AV1 bit depth.</param>
[Theory]
[InlineData(Av1ColorFormat.Yuv422, Av1BitDepth.EightBit)]
[InlineData(Av1ColorFormat.Yuv422, Av1BitDepth.TwelveBit)]
[InlineData(Av1ColorFormat.Yuv420, Av1BitDepth.EightBit)]
[InlineData(Av1ColorFormat.Yuv420, Av1BitDepth.TwelveBit)]
public void RgbToYuvSubsamplingAveragesFullResolutionChroma(int colorFormat, int bitDepth)
{
const int width = 35;
const int height = 3;
using Image<Rgb48> source = new(width, height);
for (int y = 0; y < height; y++)
{
Span<Rgb48> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < width; x++)
{
row[x] = new Rgb48(
(ushort)((x * 1879) + (y * 791)),
(ushort)((x * 977) + (y * 3251)),
(ushort)((x * 613) + (y * 4987)));
}
}
ObuSequenceHeader fullResolutionHeader = CreateSequenceHeader(
width,
height,
colorFormat: Av1ColorFormat.Yuv444,
bitDepth: (Av1BitDepth)bitDepth);
ObuSequenceHeader subsampledHeader = CreateSequenceHeader(
width,
height,
colorFormat: (Av1ColorFormat)colorFormat,
bitDepth: (Av1BitDepth)bitDepth);
using Av1FrameBuffer<byte> fullResolution = new(Configuration.Default, fullResolutionHeader, Av1ColorFormat.Yuv444, false);
using Av1FrameBuffer<byte> subsampled = new(Configuration.Default, subsampledHeader, (Av1ColorFormat)colorFormat, false);
Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, fullResolution);
Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, subsampled);
AssertSubsampledPlaneMatchesAverage(fullResolution, subsampled, Av1Plane.U);
AssertSubsampledPlaneMatchesAverage(fullResolution, subsampled, Av1Plane.V);
}
/// <summary>
/// Verifies an image-wide RGB-to-YUV-to-RGB conversion against the configured similarity tolerance.
/// </summary>
/// <param name="provider">The source test-image provider.</param>
// [Theory]
// [WithFile(TestImages.Jpeg.Baseline.Winter444_Interleaved, PixelTypes.Rgb24)]
public void RoundTrip(TestImageProvider<Rgb24> provider)
@ -448,6 +677,19 @@ public class Av1YuvConverterTests
ImageComparer.Tolerant(0.002F).VerifySimilarity(image, actual);
}
/// <summary>
/// Creates a sequence header containing the color signaling required by a conversion test.
/// </summary>
/// <param name="width">The frame width.</param>
/// <param name="height">The frame height.</param>
/// <param name="fullRange">Whether encoded samples use the full range.</param>
/// <param name="matrixCoefficients">The matrix coefficients used for conversion.</param>
/// <param name="colorFormat">The encoded plane layout.</param>
/// <param name="chromaSamplePosition">The signaled chroma sample position.</param>
/// <param name="bitDepth">The encoded sample bit depth.</param>
/// <param name="transferCharacteristics">The transfer characteristics used by nonlinear matrices.</param>
/// <param name="colorPrimaries">The color primaries used by derived matrices.</param>
/// <returns>The configured sequence header.</returns>
private static ObuSequenceHeader CreateSequenceHeader(
int width,
int height,
@ -455,7 +697,9 @@ public class Av1YuvConverterTests
ObuMatrixCoefficients matrixCoefficients = ObuMatrixCoefficients.Bt709,
Av1ColorFormat colorFormat = Av1ColorFormat.Yuv444,
ObuChromoSamplePosition chromaSamplePosition = ObuChromoSamplePosition.Unknown,
Av1BitDepth bitDepth = Av1BitDepth.EightBit)
Av1BitDepth bitDepth = Av1BitDepth.EightBit,
ObuTransferCharacteristics transferCharacteristics = ObuTransferCharacteristics.Bt709,
ObuColorPrimaries colorPrimaries = ObuColorPrimaries.Bt709)
=> new()
{
MaxFrameWidth = width,
@ -465,10 +709,83 @@ public class Av1YuvConverterTests
IsMonochrome = colorFormat == Av1ColorFormat.Yuv400,
BitDepth = bitDepth,
MatrixCoefficients = matrixCoefficients,
TransferCharacteristics = transferCharacteristics,
ColorPrimaries = colorPrimaries,
ColorRange = fullRange,
SubSamplingX = colorFormat is Av1ColorFormat.Yuv400 or Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422,
SubSamplingY = colorFormat is Av1ColorFormat.Yuv400 or Av1ColorFormat.Yuv420,
ChromaSamplePosition = chromaSamplePosition,
},
};
/// <summary>
/// Verifies that every high-bit-depth sample in a plane matches its first sample.
/// </summary>
/// <param name="frameBuffer">The encoded frame buffer.</param>
/// <param name="plane">The plane to inspect.</param>
/// <param name="subX">The horizontal subsampling shift.</param>
/// <param name="subY">The vertical subsampling shift.</param>
private static void AssertPlaneContainsRepeatedSample(Av1FrameBuffer<byte> frameBuffer, Av1Plane plane, int subX, int subY)
{
Span<ushort> samples = frameBuffer.GetHighBitDepthRowSpan(plane, 0, subX, subY);
for (int x = 1; x < samples.Length; x++)
{
Assert.Equal(samples[0], samples[x]);
}
}
/// <summary>
/// Verifies that a subsampled plane contains the rounded mean of the corresponding full-resolution samples.
/// </summary>
/// <param name="fullResolution">The full-resolution encoded frame.</param>
/// <param name="subsampled">The subsampled encoded frame.</param>
/// <param name="plane">The chroma plane to compare.</param>
private static void AssertSubsampledPlaneMatchesAverage(
Av1FrameBuffer<byte> fullResolution,
Av1FrameBuffer<byte> subsampled,
Av1Plane plane)
{
int subY = subsampled.ColorConfig.SubSamplingY ? 1 : 0;
int chromaHeight = (subsampled.Height + subY) >> subY;
int chromaWidth = (subsampled.Width + 1) >> 1;
for (int y = 0; y < chromaHeight; y++)
{
int sourceY = y << subY;
int rowCount = subY == 0 ? 1 : Math.Min(2, fullResolution.Height - sourceY);
for (int x = 0; x < chromaWidth; x++)
{
int sourceX = x << 1;
int columnCount = Math.Min(2, fullResolution.Width - sourceX);
int sum = 0;
for (int row = 0; row < rowCount; row++)
{
for (int column = 0; column < columnCount; column++)
{
sum += GetPlaneSample(fullResolution, plane, sourceX + column, sourceY + row, 0, 0);
}
}
int expected = (int)MathF.Round((float)sum / (rowCount * columnCount), MidpointRounding.AwayFromZero);
int actual = GetPlaneSample(subsampled, plane, x, y, 1, subY);
Assert.True(
actual >= expected - 1 && actual <= expected + 1,
$"Plane {plane}, sample ({x}, {y}): expected {expected} +/- 1 from sum {sum} over {rowCount * columnCount} samples but found {actual}.");
}
}
}
/// <summary>
/// Gets one encoded sample from an eight-bit or high-bit-depth frame plane.
/// </summary>
/// <param name="frameBuffer">The encoded frame buffer.</param>
/// <param name="plane">The plane containing the sample.</param>
/// <param name="x">The horizontal sample coordinate.</param>
/// <param name="y">The vertical sample coordinate.</param>
/// <param name="subX">The horizontal subsampling shift.</param>
/// <param name="subY">The vertical subsampling shift.</param>
/// <returns>The encoded sample value.</returns>
private static int GetPlaneSample(Av1FrameBuffer<byte> frameBuffer, Av1Plane plane, int x, int y, int subX, int subY)
=> frameBuffer.BitDepth == Av1BitDepth.EightBit
? frameBuffer.DeriveBlockPointer(plane, subX, subY).DangerousGetRowSpan(y)[x]
: frameBuffer.GetHighBitDepthRowSpan(plane, y, subX, subY)[x];
}

Loading…
Cancel
Save