Browse Source

Complete H.273 AV1 color conversion

pull/2633/head
James Jackson-South 1 week ago
parent
commit
570067dc33
  1. 6
      HEIF_IMPLEMENTATION_PLAN.md
  2. 34
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConversionParameters.cs
  3. 502
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.IptC2Operator.cs
  4. 222
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.YCgCoReversibleOperator.cs
  5. 12
      src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.cs
  6. 28
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs
  7. 87
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.Simd.cs
  8. 157
      src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs
  9. 15
      src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs
  10. 17
      src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs
  11. 206
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs

6
HEIF_IMPLEMENTATION_PLAN.md

@ -34,9 +34,11 @@ 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] 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, one scalar tail, and JPEG's optimized byte-plane pack/unpack contracts for eight-bit pixels.
- [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 the H.273 V4 IPT-C2 and reversible YCgCo-Re/YCgCo-Ro transforms in both directions as closed SIMD operators, including their reduced RGB precision, signed integer lifting, full- and limited-range behavior, and 4:4:4 constraint.
- [x] Verify IPT-C2 against independently calculated matrix values and verify YCgCo-Re/YCgCo-Ro against exact 8/10/12-bit code values, limited-range black/white values, and SIMD-lane/scalar-tail parity.
- [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.
@ -199,7 +201,7 @@ This snapshot pins or classifies the available references and failures; it does
| Managed implementation | Normative behavior | Reviewed implementation reference | Use |
| --- | --- | --- | --- |
| `Av1YuvConverter.ConvertToRgb`, `ConvertFromRgb`, scalar row conversion, chroma reconstruction, and chroma downsampling | ITU-T H.273 (V4) equations 14-16 and 27-84, including limited/full-range scaling, chromaticity-derived equations 39-47, YCgCo equations 51-57, constant-luminance equations 66-75, and the PQ/HLG ICtCp matrices; AV1 section 6.4.2 chroma sample positions | Official ITU-T H.273 (V4) (07/2024); libavif `src/reformat.c` and `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826`; libaom `aom/aom_image.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar behavioral oracle for full/limited-range conversion at 8, 10, and 12 bits. Decode and encode cover identity, YCgCo, coefficient-based NCL, both fixed and chromaticity-derived constant/non-constant-luminance systems, SMPTE ST 2085, and ICtCp across monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with AV1 chroma sample positioning. Limited-range YCgCo retains the 219-code scale inherited from its R/G/B inputs instead of applying YCbCr's unrelated 224-code chroma range. Chromaticity derivation uses every defined H.273 primary and matches libavif's BT.709 fallback for unspecified or reserved primaries. The ICtCp inverse is derived from the exact H.273 integer matrices rather than an unrelated display conversion. Later SIMD paths must preserve this scalar behavior. |
| `Av1YuvConverter.ConvertToRgb`, `ConvertFromRgb`, scalar row conversion, chroma reconstruction, and chroma downsampling | ITU-T H.273 (V4) equations 14-16 and 27-84, including limited/full-range scaling, chromaticity-derived equations 39-47, YCgCo equations 51-57, constant-luminance equations 66-75, IPT-C2, YCgCo-Re/YCgCo-Ro, and the PQ/HLG ICtCp matrices; AV1 section 6.4.2 chroma sample positions | Official ITU-T H.273 (V4) (07/2024); libavif `src/reformat.c` and `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826`; libaom `aom/aom_image.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar behavioral oracle for full/limited-range conversion at 8, 10, and 12 bits. Decode and encode cover every H.273 V4 matrix code point through 17: identity, YCgCo, coefficient-based NCL, both fixed and chromaticity-derived constant/non-constant-luminance systems, SMPTE ST 2085, ICtCp, IPT-C2, YCgCo-Re, and YCgCo-Ro. The common paths cover monochrome, YUV 4:2:0, 4:2:2, and 4:4:4 with AV1 chroma sample positioning; identity and the reversible lifting transforms enforce their full-chroma constraint. Limited-range YCgCo retains the 219-code scale inherited from its R/G/B inputs instead of applying YCbCr's unrelated 224-code chroma range. YCgCo-Re and YCgCo-Ro apply range adjustment at their reduced-precision RGB code values before signed integer lifting. Chromaticity derivation uses every defined H.273 primary and matches libavif's BT.709 fallback for unspecified or reserved primaries. The ICtCp and IPT-C2 inverses are derived from their exact H.273 matrices rather than unrelated display conversions. The closed operators provide `Vector512`, `Vector256`, and `Vector128` implementations with one scalar tail. |
| `Av1TransferFunctions` | ITU-T H.273 (V4) Table 3 transfer characteristics 1-18 | Official ITU-T H.273 (V4) (07/2024); libavif `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826` | Apply every AV1-signallable transfer function required by constant-luminance and ICtCp color conversion. Retain the H.273 normalized PQ and HLG definitions; do not import libavif's display-oriented 203-nit scaling or HLG OOTF into codec sample interpretation. Use libavif's midpoint convention only for the non-bijective zero code of the two logarithmic curves. |
| `ObuReader.ReadSequenceHeader`, `ReadUncompressedFrameHeader`, decoder-model parsing, and operating-parameter consumption | AV1 sections 5.5.2 through 5.5.4 sequence timing and decoder-model syntax, section 5.9.2 uncompressed frame-header syntax, and section 5.9.31 temporal-point syntax | libaom `av1/decoder/decodeframe.c` functions `av1_read_decoder_model_info`, `av1_read_op_parameters_info`, `read_temporal_point_info`, and `read_uncompressed_header`, plus `common/av1_config.c`, at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Read the normative 32-bit decoding-tick field; consume operating-point buffer delays and the layer-applicable frame removal times needed to keep a non-reduced still-image sequence aligned; and read presentation time only under the normative decoder-model condition. Retain no scheduling or playback behavior from those values and introduce no ISO BMFF timing, track, sample-table, or sequence surface. |
| `Av1FrameBuffer` high-bit-depth sample layout and `Av1YuvConverter` 10/12-bit packed-pixel conversion | AV1 section 6.4.1 bit depth and H.273 sample-range scaling | libaom `aom_scale/yv12config.h`, `av1/common/idct.c`, and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f`; libavif `src/avif.c` and `src/reformat.c` at `092276ce89098ead06db80975173191e5fee1826` | Establish two-byte native sample storage with sample-unit strides for 10/12-bit reconstruction and use ImageSharp's existing `Rgb48` pixel-operation paths in both directions so packed-pixel staging does not reduce high-bit-depth samples to eight bits. |

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

@ -22,6 +22,10 @@ internal readonly struct Av1ColorConversionParameters
/// <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="encodedSampleMaximum">The largest encoded sample value.</param>
/// <param name="rgbBias">The reversible transform's RGB code-value bias.</param>
/// <param name="rgbScale">The reversible transform's RGB code-value range.</param>
/// <param name="rgbSampleMaximum">The largest reversible transform RGB code value.</param>
public Av1ColorConversionParameters(
float kr,
float kg,
@ -31,7 +35,11 @@ internal readonly struct Av1ColorConversionParameters
float lumaBias,
float lumaScale,
float chromaBias,
float chromaScale)
float chromaScale,
float encodedSampleMaximum,
float rgbBias,
float rgbScale,
float rgbSampleMaximum)
{
this.Kr = kr;
this.Kg = kg;
@ -46,6 +54,10 @@ internal readonly struct Av1ColorConversionParameters
this.LumaScale = lumaScale;
this.ChromaBias = chromaBias;
this.ChromaScale = chromaScale;
this.EncodedSampleMaximum = encodedSampleMaximum;
this.RgbBias = rgbBias;
this.RgbScale = rgbScale;
this.RgbSampleMaximum = rgbSampleMaximum;
}
/// <summary>
@ -112,6 +124,26 @@ internal readonly struct Av1ColorConversionParameters
/// Gets the encoded chroma range.
/// </summary>
public float ChromaScale { get; }
/// <summary>
/// Gets the largest encoded sample value.
/// </summary>
public float EncodedSampleMaximum { get; }
/// <summary>
/// Gets the reversible transform's RGB code-value bias.
/// </summary>
public float RgbBias { get; }
/// <summary>
/// Gets the reversible transform's RGB code-value range.
/// </summary>
public float RgbScale { get; }
/// <summary>
/// Gets the largest reversible transform RGB code value.
/// </summary>
public float RgbSampleMaximum { get; }
}
/// <summary>

502
src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.IptC2Operator.cs

@ -0,0 +1,502 @@
// 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 IPT-C2 conversion for scalar and SIMD lanes.
/// </summary>
internal readonly struct Av1IptC2ColorOperator : IAv1ColorOperator
{
/// <summary>
/// The linear red contribution to L.
/// </summary>
public const float RedToL = 1747F / 4096F;
/// <summary>
/// The linear green contribution to L.
/// </summary>
public const float GreenToL = 2169F / 4096F;
/// <summary>
/// The linear blue contribution to L.
/// </summary>
public const float BlueToL = 180F / 4096F;
/// <summary>
/// The linear red contribution to M.
/// </summary>
public const float RedToM = 673F / 4096F;
/// <summary>
/// The linear green contribution to M.
/// </summary>
public const float GreenToM = 3029F / 4096F;
/// <summary>
/// The linear blue contribution to M.
/// </summary>
public const float BlueToM = 394F / 4096F;
/// <summary>
/// The linear red contribution to S.
/// </summary>
public const float RedToS = 50F / 4096F;
/// <summary>
/// The linear green contribution to S.
/// </summary>
public const float GreenToS = 207F / 4096F;
/// <summary>
/// The linear blue contribution to S.
/// </summary>
public const float BlueToS = 3839F / 4096F;
/// <summary>
/// The nonlinear L contribution to intensity.
/// </summary>
public const float LToIntensity = 1638F / 4096F;
/// <summary>
/// The nonlinear M contribution to intensity.
/// </summary>
public const float MToIntensity = 1638F / 4096F;
/// <summary>
/// The nonlinear S contribution to intensity.
/// </summary>
public const float SToIntensity = 820F / 4096F;
/// <summary>
/// The nonlinear L contribution to the protan axis.
/// </summary>
public const float LToProtan = 18248F / 4096F;
/// <summary>
/// The nonlinear M contribution to the protan axis.
/// </summary>
public const float MToProtan = -19870F / 4096F;
/// <summary>
/// The nonlinear S contribution to the protan axis.
/// </summary>
public const float SToProtan = 1622F / 4096F;
/// <summary>
/// The nonlinear L contribution to the tritan axis.
/// </summary>
public const float LToTritan = 3300F / 4096F;
/// <summary>
/// The nonlinear M contribution to the tritan axis.
/// </summary>
public const float MToTritan = 1463F / 4096F;
/// <summary>
/// The nonlinear S contribution to the tritan axis.
/// </summary>
public const float SToTritan = -4763F / 4096F;
/// <summary>
/// The protan contribution to nonlinear L.
/// </summary>
public const float ProtanToL = 0.0975578875686935F;
/// <summary>
/// The tritan contribution to nonlinear L.
/// </summary>
public const float TritanToL = 0.20538292958984272F;
/// <summary>
/// The protan contribution to nonlinear M.
/// </summary>
public const float ProtanToM = -0.11388362209560723F;
/// <summary>
/// The tritan contribution to nonlinear M.
/// </summary>
public const float TritanToM = 0.13337828363655785F;
/// <summary>
/// The protan contribution to nonlinear S.
/// </summary>
public const float ProtanToS = 0.032611650189127685F;
/// <summary>
/// The tritan contribution to nonlinear S.
/// </summary>
public const float TritanToS = -0.6766961795912734F;
/// <summary>
/// The linear L contribution to red.
/// </summary>
public const float LToRed = 3.2374662424353895F;
/// <summary>
/// The linear M contribution to red.
/// </summary>
public const float MToRed = -2.324205800020636F;
/// <summary>
/// The linear S contribution to red.
/// </summary>
public const float SToRed = 0.08673955758524626F;
/// <summary>
/// The linear L contribution to green.
/// </summary>
public const float LToGreen = -0.7188754693535147F;
/// <summary>
/// The linear M contribution to green.
/// </summary>
public const float MToGreen = 1.877899954242238F;
/// <summary>
/// The linear S contribution to green.
/// </summary>
public const float SToGreen = -0.1590244848887234F;
/// <summary>
/// The linear L contribution to blue.
/// </summary>
public const float LToBlue = -0.003403513926958051F;
/// <summary>
/// The linear M contribution to blue.
/// </summary>
public const float MToBlue = -0.07098593397424108F;
/// <summary>
/// The linear S contribution to blue.
/// </summary>
public const float SToBlue = 1.074389447901199F;
/// <inheritdoc/>
public static bool ChromaUsesLumaRange => false;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(ref float intensity, ref float protan, ref float tritan, in Av1ColorConversionParameters parameters)
{
// IPT-C2 stores opponent axes around intensity in nonlinear LMS. Undo both matrices around the
// signaled transfer function so the final RGB values remain in the source signal domain.
float nonlinearL = intensity + (ProtanToL * protan) + (TritanToL * tritan);
float nonlinearM = intensity + (ProtanToM * protan) + (TritanToM * tritan);
float nonlinearS = intensity + (ProtanToS * protan) + (TritanToS * tritan);
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);
protan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
tritan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector128<float> intensity,
ref Vector128<float> protan,
ref Vector128<float> tritan,
in Av1ColorConversionParameters parameters)
{
Vector128<float> nonlinearL = Vector128.MultiplyAddEstimate(
Vector128.Create(TritanToL),
tritan,
Vector128.MultiplyAddEstimate(Vector128.Create(ProtanToL), protan, intensity));
Vector128<float> nonlinearM = Vector128.MultiplyAddEstimate(
Vector128.Create(TritanToM),
tritan,
Vector128.MultiplyAddEstimate(Vector128.Create(ProtanToM), protan, intensity));
Vector128<float> nonlinearS = Vector128.MultiplyAddEstimate(
Vector128.Create(TritanToS),
tritan,
Vector128.MultiplyAddEstimate(Vector128.Create(ProtanToS), protan, 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);
protan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
tritan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector256<float> intensity,
ref Vector256<float> protan,
ref Vector256<float> tritan,
in Av1ColorConversionParameters parameters)
{
Vector256<float> nonlinearL = Vector256.MultiplyAddEstimate(
Vector256.Create(TritanToL),
tritan,
Vector256.MultiplyAddEstimate(Vector256.Create(ProtanToL), protan, intensity));
Vector256<float> nonlinearM = Vector256.MultiplyAddEstimate(
Vector256.Create(TritanToM),
tritan,
Vector256.MultiplyAddEstimate(Vector256.Create(ProtanToM), protan, intensity));
Vector256<float> nonlinearS = Vector256.MultiplyAddEstimate(
Vector256.Create(TritanToS),
tritan,
Vector256.MultiplyAddEstimate(Vector256.Create(ProtanToS), protan, 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);
protan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
tritan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertToRgb(
ref Vector512<float> intensity,
ref Vector512<float> protan,
ref Vector512<float> tritan,
in Av1ColorConversionParameters parameters)
{
Vector512<float> nonlinearL = Vector512.MultiplyAddEstimate(
Vector512.Create(TritanToL),
tritan,
Vector512.MultiplyAddEstimate(Vector512.Create(ProtanToL), protan, intensity));
Vector512<float> nonlinearM = Vector512.MultiplyAddEstimate(
Vector512.Create(TritanToM),
tritan,
Vector512.MultiplyAddEstimate(Vector512.Create(ProtanToM), protan, intensity));
Vector512<float> nonlinearS = Vector512.MultiplyAddEstimate(
Vector512.Create(TritanToS),
tritan,
Vector512.MultiplyAddEstimate(Vector512.Create(ProtanToS), protan, 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);
protan = Av1TransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen);
tritan = 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 protan,
out float tritan)
{
// The encoded RGB signal is linearized before the LMS matrix, then the signaled transfer function
// is reapplied to each LMS component before the fixed IPT-C2 opponent matrix.
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 = (LToIntensity * nonlinearL) + (MToIntensity * nonlinearM) + (SToIntensity * nonlinearS);
protan = (LToProtan * nonlinearL) + (MToProtan * nonlinearM) + (SToProtan * nonlinearS);
tritan = (LToTritan * nonlinearL) + (MToTritan * nonlinearM) + (SToTritan * 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> protan,
out Vector128<float> tritan)
{
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(BlueToL),
linearBlue,
Vector128.MultiplyAddEstimate(Vector128.Create(GreenToL), linearGreen, Vector128.Create(RedToL) * linearRed));
Vector128<float> linearM = Vector128.MultiplyAddEstimate(
Vector128.Create(BlueToM),
linearBlue,
Vector128.MultiplyAddEstimate(Vector128.Create(GreenToM), linearGreen, Vector128.Create(RedToM) * linearRed));
Vector128<float> linearS = Vector128.MultiplyAddEstimate(
Vector128.Create(BlueToS),
linearBlue,
Vector128.MultiplyAddEstimate(Vector128.Create(GreenToS), linearGreen, Vector128.Create(RedToS) * linearRed));
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.MultiplyAddEstimate(
Vector128.Create(SToIntensity),
nonlinearS,
Vector128.MultiplyAddEstimate(Vector128.Create(MToIntensity), nonlinearM, Vector128.Create(LToIntensity) * nonlinearL));
protan = Vector128.MultiplyAddEstimate(
Vector128.Create(SToProtan),
nonlinearS,
Vector128.MultiplyAddEstimate(Vector128.Create(MToProtan), nonlinearM, Vector128.Create(LToProtan) * nonlinearL));
tritan = Vector128.MultiplyAddEstimate(
Vector128.Create(SToTritan),
nonlinearS,
Vector128.MultiplyAddEstimate(Vector128.Create(MToTritan), nonlinearM, Vector128.Create(LToTritan) * nonlinearL));
}
/// <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> protan,
out Vector256<float> tritan)
{
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(BlueToL),
linearBlue,
Vector256.MultiplyAddEstimate(Vector256.Create(GreenToL), linearGreen, Vector256.Create(RedToL) * linearRed));
Vector256<float> linearM = Vector256.MultiplyAddEstimate(
Vector256.Create(BlueToM),
linearBlue,
Vector256.MultiplyAddEstimate(Vector256.Create(GreenToM), linearGreen, Vector256.Create(RedToM) * linearRed));
Vector256<float> linearS = Vector256.MultiplyAddEstimate(
Vector256.Create(BlueToS),
linearBlue,
Vector256.MultiplyAddEstimate(Vector256.Create(GreenToS), linearGreen, Vector256.Create(RedToS) * linearRed));
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.MultiplyAddEstimate(
Vector256.Create(SToIntensity),
nonlinearS,
Vector256.MultiplyAddEstimate(Vector256.Create(MToIntensity), nonlinearM, Vector256.Create(LToIntensity) * nonlinearL));
protan = Vector256.MultiplyAddEstimate(
Vector256.Create(SToProtan),
nonlinearS,
Vector256.MultiplyAddEstimate(Vector256.Create(MToProtan), nonlinearM, Vector256.Create(LToProtan) * nonlinearL));
tritan = Vector256.MultiplyAddEstimate(
Vector256.Create(SToTritan),
nonlinearS,
Vector256.MultiplyAddEstimate(Vector256.Create(MToTritan), nonlinearM, Vector256.Create(LToTritan) * nonlinearL));
}
/// <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> protan,
out Vector512<float> tritan)
{
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(BlueToL),
linearBlue,
Vector512.MultiplyAddEstimate(Vector512.Create(GreenToL), linearGreen, Vector512.Create(RedToL) * linearRed));
Vector512<float> linearM = Vector512.MultiplyAddEstimate(
Vector512.Create(BlueToM),
linearBlue,
Vector512.MultiplyAddEstimate(Vector512.Create(GreenToM), linearGreen, Vector512.Create(RedToM) * linearRed));
Vector512<float> linearS = Vector512.MultiplyAddEstimate(
Vector512.Create(BlueToS),
linearBlue,
Vector512.MultiplyAddEstimate(Vector512.Create(GreenToS), linearGreen, Vector512.Create(RedToS) * linearRed));
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.MultiplyAddEstimate(
Vector512.Create(SToIntensity),
nonlinearS,
Vector512.MultiplyAddEstimate(Vector512.Create(MToIntensity), nonlinearM, Vector512.Create(LToIntensity) * nonlinearL));
protan = Vector512.MultiplyAddEstimate(
Vector512.Create(SToProtan),
nonlinearS,
Vector512.MultiplyAddEstimate(Vector512.Create(MToProtan), nonlinearM, Vector512.Create(LToProtan) * nonlinearL));
tritan = Vector512.MultiplyAddEstimate(
Vector512.Create(SToTritan),
nonlinearS,
Vector512.MultiplyAddEstimate(Vector512.Create(MToTritan), nonlinearM, Vector512.Create(LToTritan) * nonlinearL));
}
}
}

222
src/ImageSharp/Formats/Heif/Av1/Av1ColorConverter.YCgCoReversibleOperator.cs

@ -0,0 +1,222 @@
// 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 the YCgCo-Re and YCgCo-Ro integer lifting transforms for scalar and SIMD lanes.
/// </summary>
internal readonly struct Av1YCgCoReversibleColorOperator : 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)
{
int yCode = (int)MathF.Floor((y * parameters.EncodedSampleMaximum) + 0.5F);
int cgCode = (int)MathF.Floor((cg * parameters.EncodedSampleMaximum) + 0.5F);
int coCode = (int)MathF.Floor((co * parameters.EncodedSampleMaximum) + 0.5F);
// Signed arithmetic shifts are part of the reversible lifting definition. In particular, they preserve
// the specified floor division for negative odd Cg and Co values instead of truncating toward zero.
int temporary = yCode - (cgCode >> 1);
int greenCode = Numerics.Clamp(temporary + cgCode, 0, (int)parameters.RgbSampleMaximum);
int blueCode = Numerics.Clamp(temporary - (coCode >> 1), 0, (int)parameters.RgbSampleMaximum);
int redCode = Numerics.Clamp(blueCode + coCode, 0, (int)parameters.RgbSampleMaximum);
float inverseRgbScale = 1F / parameters.RgbScale;
y = (redCode - parameters.RgbBias) * inverseRgbScale;
cg = (greenCode - parameters.RgbBias) * inverseRgbScale;
co = (blueCode - parameters.RgbBias) * inverseRgbScale;
}
/// <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> encodedMaximum = Vector128.Create(parameters.EncodedSampleMaximum);
Vector128<float> half = Vector128.Create(0.5F);
Vector128<int> yCode = Vector128.ConvertToInt32(Vector128.Floor((y * encodedMaximum) + half));
Vector128<int> cgCode = Vector128.ConvertToInt32(Vector128.Floor((cg * encodedMaximum) + half));
Vector128<int> coCode = Vector128.ConvertToInt32(Vector128.Floor((co * encodedMaximum) + half));
// Integer lanes preserve the normative arithmetic shifts; converting the lifting stages back to
// floating point would change negative odd Cg and Co values and break reversibility.
Vector128<int> temporary = yCode - Vector128.ShiftRightArithmetic(cgCode, 1);
Vector128<int> zero = Vector128<int>.Zero;
Vector128<int> rgbMaximum = Vector128.Create((int)parameters.RgbSampleMaximum);
Vector128<int> greenCode = Vector128.Min(Vector128.Max(temporary + cgCode, zero), rgbMaximum);
Vector128<int> blueCode = Vector128.Min(Vector128.Max(temporary - Vector128.ShiftRightArithmetic(coCode, 1), zero), rgbMaximum);
Vector128<int> redCode = Vector128.Min(Vector128.Max(blueCode + coCode, zero), rgbMaximum);
Vector128<float> rgbBias = Vector128.Create(parameters.RgbBias);
Vector128<float> inverseRgbScale = Vector128.Create(1F / parameters.RgbScale);
y = (Vector128.ConvertToSingle(redCode) - rgbBias) * inverseRgbScale;
cg = (Vector128.ConvertToSingle(greenCode) - rgbBias) * inverseRgbScale;
co = (Vector128.ConvertToSingle(blueCode) - rgbBias) * inverseRgbScale;
}
/// <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> encodedMaximum = Vector256.Create(parameters.EncodedSampleMaximum);
Vector256<float> half = Vector256.Create(0.5F);
Vector256<int> yCode = Vector256.ConvertToInt32(Vector256.Floor((y * encodedMaximum) + half));
Vector256<int> cgCode = Vector256.ConvertToInt32(Vector256.Floor((cg * encodedMaximum) + half));
Vector256<int> coCode = Vector256.ConvertToInt32(Vector256.Floor((co * encodedMaximum) + half));
Vector256<int> temporary = yCode - Vector256.ShiftRightArithmetic(cgCode, 1);
Vector256<int> zero = Vector256<int>.Zero;
Vector256<int> rgbMaximum = Vector256.Create((int)parameters.RgbSampleMaximum);
Vector256<int> greenCode = Vector256.Min(Vector256.Max(temporary + cgCode, zero), rgbMaximum);
Vector256<int> blueCode = Vector256.Min(Vector256.Max(temporary - Vector256.ShiftRightArithmetic(coCode, 1), zero), rgbMaximum);
Vector256<int> redCode = Vector256.Min(Vector256.Max(blueCode + coCode, zero), rgbMaximum);
Vector256<float> rgbBias = Vector256.Create(parameters.RgbBias);
Vector256<float> inverseRgbScale = Vector256.Create(1F / parameters.RgbScale);
y = (Vector256.ConvertToSingle(redCode) - rgbBias) * inverseRgbScale;
cg = (Vector256.ConvertToSingle(greenCode) - rgbBias) * inverseRgbScale;
co = (Vector256.ConvertToSingle(blueCode) - rgbBias) * inverseRgbScale;
}
/// <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> encodedMaximum = Vector512.Create(parameters.EncodedSampleMaximum);
Vector512<float> half = Vector512.Create(0.5F);
Vector512<int> yCode = Vector512.ConvertToInt32(Vector512.Floor((y * encodedMaximum) + half));
Vector512<int> cgCode = Vector512.ConvertToInt32(Vector512.Floor((cg * encodedMaximum) + half));
Vector512<int> coCode = Vector512.ConvertToInt32(Vector512.Floor((co * encodedMaximum) + half));
Vector512<int> temporary = yCode - Vector512.ShiftRightArithmetic(cgCode, 1);
Vector512<int> zero = Vector512<int>.Zero;
Vector512<int> rgbMaximum = Vector512.Create((int)parameters.RgbSampleMaximum);
Vector512<int> greenCode = Vector512.Min(Vector512.Max(temporary + cgCode, zero), rgbMaximum);
Vector512<int> blueCode = Vector512.Min(Vector512.Max(temporary - Vector512.ShiftRightArithmetic(coCode, 1), zero), rgbMaximum);
Vector512<int> redCode = Vector512.Min(Vector512.Max(blueCode + coCode, zero), rgbMaximum);
Vector512<float> rgbBias = Vector512.Create(parameters.RgbBias);
Vector512<float> inverseRgbScale = Vector512.Create(1F / parameters.RgbScale);
y = (Vector512.ConvertToSingle(redCode) - rgbBias) * inverseRgbScale;
cg = (Vector512.ConvertToSingle(greenCode) - rgbBias) * inverseRgbScale;
co = (Vector512.ConvertToSingle(blueCode) - rgbBias) * inverseRgbScale;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
float red,
float green,
float blue,
in Av1ColorConversionParameters parameters,
out float y,
out float cg,
out float co)
{
int redCode = (int)MathF.Floor((red * parameters.RgbScale) + parameters.RgbBias + 0.5F);
int greenCode = (int)MathF.Floor((green * parameters.RgbScale) + parameters.RgbBias + 0.5F);
int blueCode = (int)MathF.Floor((blue * parameters.RgbScale) + parameters.RgbBias + 0.5F);
int coCode = redCode - blueCode;
int temporary = blueCode + (coCode >> 1);
int cgCode = greenCode - temporary;
int yCode = temporary + (cgCode >> 1);
float inverseEncodedMaximum = 1F / parameters.EncodedSampleMaximum;
y = yCode * inverseEncodedMaximum;
cg = cgCode * inverseEncodedMaximum;
co = coCode * inverseEncodedMaximum;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector128<float> red,
Vector128<float> green,
Vector128<float> blue,
in Av1ColorConversionParameters parameters,
out Vector128<float> y,
out Vector128<float> cg,
out Vector128<float> co)
{
Vector128<float> rgbScale = Vector128.Create(parameters.RgbScale);
Vector128<float> quantizationOffset = Vector128.Create(parameters.RgbBias + 0.5F);
Vector128<int> redCode = Vector128.ConvertToInt32(Vector128.Floor((red * rgbScale) + quantizationOffset));
Vector128<int> greenCode = Vector128.ConvertToInt32(Vector128.Floor((green * rgbScale) + quantizationOffset));
Vector128<int> blueCode = Vector128.ConvertToInt32(Vector128.Floor((blue * rgbScale) + quantizationOffset));
Vector128<int> coCode = redCode - blueCode;
Vector128<int> temporary = blueCode + Vector128.ShiftRightArithmetic(coCode, 1);
Vector128<int> cgCode = greenCode - temporary;
Vector128<int> yCode = temporary + Vector128.ShiftRightArithmetic(cgCode, 1);
Vector128<float> inverseEncodedMaximum = Vector128.Create(1F / parameters.EncodedSampleMaximum);
y = Vector128.ConvertToSingle(yCode) * inverseEncodedMaximum;
cg = Vector128.ConvertToSingle(cgCode) * inverseEncodedMaximum;
co = Vector128.ConvertToSingle(coCode) * inverseEncodedMaximum;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector256<float> red,
Vector256<float> green,
Vector256<float> blue,
in Av1ColorConversionParameters parameters,
out Vector256<float> y,
out Vector256<float> cg,
out Vector256<float> co)
{
Vector256<float> rgbScale = Vector256.Create(parameters.RgbScale);
Vector256<float> quantizationOffset = Vector256.Create(parameters.RgbBias + 0.5F);
Vector256<int> redCode = Vector256.ConvertToInt32(Vector256.Floor((red * rgbScale) + quantizationOffset));
Vector256<int> greenCode = Vector256.ConvertToInt32(Vector256.Floor((green * rgbScale) + quantizationOffset));
Vector256<int> blueCode = Vector256.ConvertToInt32(Vector256.Floor((blue * rgbScale) + quantizationOffset));
Vector256<int> coCode = redCode - blueCode;
Vector256<int> temporary = blueCode + Vector256.ShiftRightArithmetic(coCode, 1);
Vector256<int> cgCode = greenCode - temporary;
Vector256<int> yCode = temporary + Vector256.ShiftRightArithmetic(cgCode, 1);
Vector256<float> inverseEncodedMaximum = Vector256.Create(1F / parameters.EncodedSampleMaximum);
y = Vector256.ConvertToSingle(yCode) * inverseEncodedMaximum;
cg = Vector256.ConvertToSingle(cgCode) * inverseEncodedMaximum;
co = Vector256.ConvertToSingle(coCode) * inverseEncodedMaximum;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ConvertFromRgb(
Vector512<float> red,
Vector512<float> green,
Vector512<float> blue,
in Av1ColorConversionParameters parameters,
out Vector512<float> y,
out Vector512<float> cg,
out Vector512<float> co)
{
Vector512<float> rgbScale = Vector512.Create(parameters.RgbScale);
Vector512<float> quantizationOffset = Vector512.Create(parameters.RgbBias + 0.5F);
Vector512<int> redCode = Vector512.ConvertToInt32(Vector512.Floor((red * rgbScale) + quantizationOffset));
Vector512<int> greenCode = Vector512.ConvertToInt32(Vector512.Floor((green * rgbScale) + quantizationOffset));
Vector512<int> blueCode = Vector512.ConvertToInt32(Vector512.Floor((blue * rgbScale) + quantizationOffset));
Vector512<int> coCode = redCode - blueCode;
Vector512<int> temporary = blueCode + Vector512.ShiftRightArithmetic(coCode, 1);
Vector512<int> cgCode = greenCode - temporary;
Vector512<int> yCode = temporary + Vector512.ShiftRightArithmetic(cgCode, 1);
Vector512<float> inverseEncodedMaximum = Vector512.Create(1F / parameters.EncodedSampleMaximum);
y = Vector512.ConvertToSingle(yCode) * inverseEncodedMaximum;
cg = Vector512.ConvertToSingle(cgCode) * inverseEncodedMaximum;
co = Vector512.ConvertToSingle(coCode) * inverseEncodedMaximum;
}
}
}

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

@ -37,6 +37,16 @@ internal enum Av1ColorConversionMode
/// The BT.2100 ICtCp color transform.
/// </summary>
ICtCp,
/// <summary>
/// The IPT-C2 color transform.
/// </summary>
IptC2,
/// <summary>
/// The YCgCo-Re or YCgCo-Ro reversible color transform.
/// </summary>
YCgCoReversible,
}
/// <summary>
@ -121,6 +131,8 @@ internal abstract partial class Av1ColorConverterBase
Av1ColorConversionMode.Smpte2085 => new Av1ColorConverter<Av1Smpte2085ColorOperator>(in parameters, isMonochrome),
Av1ColorConversionMode.ConstantLuminance => new Av1ColorConverter<Av1ConstantLuminanceColorOperator>(in parameters, isMonochrome),
Av1ColorConversionMode.ICtCp => new Av1ColorConverter<Av1ICtCpColorOperator>(in parameters, isMonochrome),
Av1ColorConversionMode.IptC2 => new Av1ColorConverter<Av1IptC2ColorOperator>(in parameters, isMonochrome),
Av1ColorConversionMode.YCgCoReversible => new Av1ColorConverter<Av1YCgCoReversibleColorOperator>(in parameters, isMonochrome),
_ => new Av1ColorConverter<Av1CoefficientColorOperator>(in parameters, isMonochrome),
};
}

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

@ -3,6 +3,7 @@
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -114,7 +115,7 @@ internal static partial class Av1YuvConverter
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.
// rows, and the final one or two float-sized slots per pixel back RGB byte planes or an Rgba64 row.
int rowCount = this.isMonochrome ? 3 : 5;
int packedRowCount = typeof(TSample) == typeof(byte) ? 1 : 2;
return this.image.Width * (rowCount + packedRowCount);
@ -126,7 +127,8 @@ internal static partial class Av1YuvConverter
/// </summary>
/// <param name="y">The row index.</param>
/// <param name="span">The reusable conversion buffer.</param>
public void Convert(int y, Span<float> span)
/// <param name="proxy">The padded destination used when an eight-bit image row cannot expose sufficient padding.</param>
public void Convert(int y, Span<float> span, Span<TPixel> proxy)
{
int width = this.image.Width;
Span<float> red = span[..width];
@ -189,9 +191,25 @@ internal static partial class Av1YuvConverter
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);
// Match JPEG's byte-plane packing contract so optimized pixel types use the existing RGB packer.
// The final float row provides enough byte storage for three tightly packed component planes.
Span<byte> byteStorage = MemoryMarshal.AsBytes(packedStorage)[..(width * 3)];
Span<byte> redBytes = byteStorage[..width];
Span<byte> greenBytes = byteStorage.Slice(width, width);
Span<byte> blueBytes = byteStorage.Slice(width * 2, width);
SimdUtils.NormalizedFloatToByteSaturate(red, redBytes);
SimdUtils.NormalizedFloatToByteSaturate(green, greenBytes);
SimdUtils.NormalizedFloatToByteSaturate(blue, blueBytes);
if (this.image.PixelBuffer.DangerousTryGetPaddedRowSpan(y, 3, out Span<TPixel> paddedDestination))
{
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, paddedDestination);
}
else
{
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, proxy);
proxy[..width].CopyTo(destination);
}
}
else
{

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

@ -601,71 +601,6 @@ internal static partial class Av1YuvConverter
}
}
/// <summary>
/// Packs normalized RGB component rows into opaque eight-bit RGBA pixels.
/// </summary>
/// <param name="red">The normalized red components.</param>
/// <param name="green">The normalized green components.</param>
/// <param name="blue">The normalized blue components.</param>
/// <param name="destination">The destination pixels.</param>
private static void PackRgba32(ReadOnlySpan<float> red, ReadOnlySpan<float> green, ReadOnlySpan<float> blue, Span<Rgba32> destination)
{
ref float redBase = ref MemoryMarshal.GetReference(red);
ref float greenBase = ref MemoryMarshal.GetReference(green);
ref float blueBase = ref MemoryMarshal.GetReference(blue);
ref Rgba32 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<int> r = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref redBase, i)), ByteMaximum);
Vector512<int> g = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum);
Vector512<int> b = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector512<float>>(ref Unsafe.Add(ref blueBase, i)), ByteMaximum);
StoreRgba32Batch(r.GetLower().GetLower(), g.GetLower().GetLower(), b.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, i));
StoreRgba32Batch(r.GetLower().GetUpper(), g.GetLower().GetUpper(), b.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4));
StoreRgba32Batch(r.GetUpper().GetLower(), g.GetUpper().GetLower(), b.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, i + 8));
StoreRgba32Batch(r.GetUpper().GetUpper(), g.GetUpper().GetUpper(), b.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 12));
}
}
if (Vector256.IsHardwareAccelerated)
{
int oneVectorFromEnd = length - Vector256<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector256<float>.Count)
{
Vector256<int> r = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref redBase, i)), ByteMaximum);
Vector256<int> g = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum);
Vector256<int> b = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector256<float>>(ref Unsafe.Add(ref blueBase, i)), ByteMaximum);
StoreRgba32Batch(r.GetLower(), g.GetLower(), b.GetLower(), ref Unsafe.Add(ref destinationBase, i));
StoreRgba32Batch(r.GetUpper(), g.GetUpper(), b.GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4));
}
}
if (Vector128.IsHardwareAccelerated)
{
int oneVectorFromEnd = length - Vector128<float>.Count;
for (; i <= oneVectorFromEnd; i += Vector128<float>.Count)
{
Vector128<int> r = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref redBase, i)), ByteMaximum);
Vector128<int> g = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref greenBase, i)), ByteMaximum);
Vector128<int> b = ScaleRoundAndClampToInt32(Unsafe.As<float, Vector128<float>>(ref Unsafe.Add(ref blueBase, i)), ByteMaximum);
StoreRgba32Batch(r, g, b, ref Unsafe.Add(ref destinationBase, i));
}
}
for (; i < length; i++)
{
Unsafe.Add(ref destinationBase, i) = new Rgba32(
ToSample<byte>(Unsafe.Add(ref redBase, i) * ByteMaximum, ByteMaximum),
ToSample<byte>(Unsafe.Add(ref greenBase, i) * ByteMaximum, ByteMaximum),
ToSample<byte>(Unsafe.Add(ref blueBase, i) * ByteMaximum, ByteMaximum));
}
}
/// <summary>
/// Packs normalized RGB component rows into opaque 16-bit RGBA pixels.
/// </summary>
@ -816,28 +751,6 @@ internal static partial class Av1YuvConverter
return Vector512.ConvertToInt32(Vector512.Round(bounded, MidpointRounding.AwayFromZero));
}
/// <summary>
/// Interleaves four red, green, and blue integer lanes into four opaque eight-bit RGBA pixels.
/// </summary>
/// <param name="red">The red component values.</param>
/// <param name="green">The green component values.</param>
/// <param name="blue">The blue component values.</param>
/// <param name="destination">The first destination pixel.</param>
private static void StoreRgba32Batch(Vector128<int> red, Vector128<int> green, Vector128<int> blue, ref Rgba32 destination)
{
Vector128<byte> red8 = Vector128.Narrow(Vector128.Narrow(red.AsUInt32(), Vector128<uint>.Zero), Vector128<ushort>.Zero);
Vector128<byte> green8 = Vector128.Narrow(Vector128.Narrow(green.AsUInt32(), Vector128<uint>.Zero), Vector128<ushort>.Zero);
Vector128<byte> blue8 = Vector128.Narrow(Vector128.Narrow(blue.AsUInt32(), Vector128<uint>.Zero), Vector128<ushort>.Zero);
Vector128<byte> alpha8 = Vector128.Create(byte.MaxValue);
Vector128<byte> redGreen = Vector128_.UnpackLow(red8, green8);
Vector128<byte> blueAlpha = Vector128_.UnpackLow(blue8, alpha8);
// Interleaving the byte pairs as 16-bit lanes produces RGBA memory order on every supported
// architecture without relying on the host integer endianness of Rgba32.PackedValue.
Vector128<byte> rgba = Vector128_.UnpackLow(redGreen.AsInt16(), blueAlpha.AsInt16()).AsByte();
Unsafe.As<Rgba32, Vector128<byte>>(ref destination) = rgba;
}
/// <summary>
/// Interleaves four red, green, and blue integer lanes into four opaque 16-bit RGBA pixels.
/// </summary>

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

@ -36,43 +36,19 @@ internal static partial class Av1YuvConverter
public static void ConvertToRgb<TPixel>(Configuration configuration, Av1FrameBuffer<byte> frameBuffer, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
GetConversionParameters(
frameBuffer,
out Av1ColorConversionMode mode,
out float kr,
out float kg,
out float kb,
out float lumaBias,
out float lumaScale,
out float chromaBias,
out float chromaScale,
out float sampleMaximum);
ObuTransferCharacteristics transferCharacteristics = frameBuffer.ColorConfig.TransferCharacteristics;
Av1ConstantLuminanceScales constantLuminanceScales = mode == Av1ColorConversionMode.ConstantLuminance
? new Av1ConstantLuminanceScales(transferCharacteristics, kr, kb)
: default;
Av1ColorConversionParameters parameters = new(
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
lumaBias,
lumaScale,
chromaBias,
chromaScale);
Av1ColorConversionParameters parameters = GetConversionParameters(frameBuffer, out Av1ColorConversionMode mode);
Av1ColorConverterBase colorConverter = Av1ColorConverterBase.Create(mode, in parameters, frameBuffer.ColorFormat == Av1ColorFormat.Yuv400);
if (frameBuffer.BitDepth == Av1BitDepth.EightBit)
{
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();
using IMemoryOwner<float> scratchOwner = configuration.MemoryAllocator.Allocate<float>(converter.BufferLength);
using IMemoryOwner<TPixel> proxyOwner = configuration.MemoryAllocator.Allocate<TPixel>(image.Width + 3);
Span<float> scratch = scratchOwner.GetSpan();
Span<TPixel> proxy = proxyOwner.GetSpan()[..(image.Width + 3)];
for (int y = 0; y < image.Height; y++)
{
converter.Convert(y, scratch);
converter.Convert(y, scratch, proxy);
}
}
else
@ -82,7 +58,7 @@ internal static partial class Av1YuvConverter
Span<float> scratch = owner.GetSpan();
for (int y = 0; y < image.Height; y++)
{
converter.Convert(y, scratch);
converter.Convert(y, scratch, Span<TPixel>.Empty);
}
}
}
@ -97,33 +73,8 @@ internal static partial class Av1YuvConverter
public static void ConvertFromRgb<TPixel>(Configuration configuration, ImageFrame<TPixel> image, Av1FrameBuffer<byte> frameBuffer)
where TPixel : unmanaged, IPixel<TPixel>
{
GetConversionParameters(
frameBuffer,
out Av1ColorConversionMode mode,
out float kr,
out float kg,
out float kb,
out float lumaBias,
out float lumaScale,
out float chromaBias,
out float chromaScale,
out float sampleMaximum);
ObuTransferCharacteristics transferCharacteristics = frameBuffer.ColorConfig.TransferCharacteristics;
Av1ConstantLuminanceScales constantLuminanceScales = mode == Av1ColorConversionMode.ConstantLuminance
? new Av1ConstantLuminanceScales(transferCharacteristics, kr, kb)
: default;
Av1ColorConversionParameters parameters = new(
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
lumaBias,
lumaScale,
chromaBias,
chromaScale);
Av1ColorConversionParameters parameters = GetConversionParameters(frameBuffer, out Av1ColorConversionMode mode);
float sampleMaximum = parameters.EncodedSampleMaximum;
bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
Av1ColorConverterBase colorConverter = Av1ColorConverterBase.Create(mode, in parameters, isMonochrome);
@ -158,31 +109,14 @@ internal static partial class Av1YuvConverter
/// </summary>
/// <param name="frameBuffer">The AV1 frame containing the signaled color configuration.</param>
/// <param name="mode">The resolved 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="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 GetConversionParameters(
Av1FrameBuffer<byte> frameBuffer,
out Av1ColorConversionMode mode,
out float kr,
out float kg,
out float kb,
out float lumaBias,
out float lumaScale,
out float chromaBias,
out float chromaScale,
out float sampleMaximum)
/// <returns>The resolved conversion parameters.</returns>
private static Av1ColorConversionParameters GetConversionParameters(Av1FrameBuffer<byte> frameBuffer, out Av1ColorConversionMode mode)
{
mode = Av1ColorConversionMode.Coefficients;
kr = 0F;
kb = 0F;
float kr = 0F;
float kb = 0F;
// These values are the matrix table used by libavif and are defined by H.273.
// These code points and fixed coefficient matrices are defined by H.273.
switch (frameBuffer.ColorConfig.MatrixCoefficients)
{
case ObuMatrixCoefficients.Identity:
@ -232,16 +166,24 @@ internal static partial class Av1YuvConverter
case ObuMatrixCoefficients.Bt2100ICtCp:
mode = Av1ColorConversionMode.ICtCp;
break;
case ObuMatrixCoefficients.IptC2:
mode = Av1ColorConversionMode.IptC2;
break;
case ObuMatrixCoefficients.YCgCoRe:
case ObuMatrixCoefficients.YCgCoRo:
mode = Av1ColorConversionMode.YCgCoReversible;
break;
default:
throw new NotSupportedException($"AV1 matrix coefficients '{frameBuffer.ColorConfig.MatrixCoefficients}' are not currently supported.");
}
kg = 1F - kr - kb;
float kg = 1F - kr - kb;
bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400;
bool isFullRange = frameBuffer.ColorConfig.ColorRange;
if (mode == Av1ColorConversionMode.Identity && !isMonochrome && frameBuffer.ColorFormat != Av1ColorFormat.Yuv444)
bool requiresFullChroma = mode is Av1ColorConversionMode.Identity or Av1ColorConversionMode.YCgCoReversible;
if (requiresFullChroma && !isMonochrome && frameBuffer.ColorFormat != Av1ColorFormat.Yuv444)
{
throw new InvalidImageContentException("AV1 identity matrix coefficients require YUV 4:4:4 sampling.");
throw new InvalidImageContentException($"AV1 {frameBuffer.ColorConfig.MatrixCoefficients} matrix coefficients require YUV 4:4:4 sampling.");
}
if (frameBuffer.ColorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Reserved)
@ -251,16 +193,55 @@ internal static partial class Av1YuvConverter
int bitCount = frameBuffer.BitDepth.GetBitCount();
int depthScale = 1 << (bitCount - 8);
sampleMaximum = (1 << bitCount) - 1;
chromaBias = 128F * depthScale;
lumaBias = isFullRange ? 0F : 16F * depthScale;
lumaScale = isFullRange ? sampleMaximum : 219F * depthScale;
float sampleMaximum = (1 << bitCount) - 1;
float chromaBias = 128F * depthScale;
float lumaBias = isFullRange ? 0F : 16F * depthScale;
float lumaScale = isFullRange ? sampleMaximum : 219F * depthScale;
// 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 == Av1ColorConversionMode.YCgCo
float chromaScale = isFullRange || mode == Av1ColorConversionMode.YCgCo
? lumaScale
: 224F * depthScale;
float rgbBias = 0F;
float rgbScale = 1F;
float rgbSampleMaximum = 1F;
if (mode == Av1ColorConversionMode.YCgCoReversible)
{
int bitOffset = frameBuffer.ColorConfig.MatrixCoefficients == ObuMatrixCoefficients.YCgCoRe ? 2 : 1;
int rgbBitCount = bitCount - bitOffset;
float rgbDepthScale = MathF.ScaleB(1F, rgbBitCount - 8);
rgbSampleMaximum = (1 << rgbBitCount) - 1;
rgbBias = isFullRange ? 0F : 16F * rgbDepthScale;
rgbScale = isFullRange ? rgbSampleMaximum : 219F * rgbDepthScale;
// The reversible lifting transform operates on raw integer code values. Range adjustment therefore
// belongs inside the operator, while the row converter only normalizes the encoded Y, Cg, and Co planes.
lumaBias = 0F;
lumaScale = sampleMaximum;
chromaScale = sampleMaximum;
}
ObuTransferCharacteristics transferCharacteristics = frameBuffer.ColorConfig.TransferCharacteristics;
Av1ConstantLuminanceScales constantLuminanceScales = mode == Av1ColorConversionMode.ConstantLuminance
? new Av1ConstantLuminanceScales(transferCharacteristics, kr, kb)
: default;
return new Av1ColorConversionParameters(
kr,
kg,
kb,
transferCharacteristics,
in constantLuminanceScales,
lumaBias,
lumaScale,
chromaBias,
chromaScale,
sampleMaximum,
rgbBias,
rgbScale,
rgbSampleMaximum);
}
/// <summary>

15
src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs

@ -77,4 +77,19 @@ internal enum ObuMatrixCoefficients
/// ITU-R BT.2100 ICtCp coefficients.
/// </summary>
Bt2100ICtCp = 14,
/// <summary>
/// IPT-C2 coefficients.
/// </summary>
IptC2 = 15,
/// <summary>
/// Reversible YCgCo coefficients with two additional encoded bits.
/// </summary>
YCgCoRe = 16,
/// <summary>
/// Reversible YCgCo coefficients with one additional encoded bit.
/// </summary>
YCgCoRo = 17,
}

17
src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Cicp;
#pragma warning disable CA1707 // Underscores in enum members
/// <summary>
/// Matrix coefficients according to ITU-T H.273 / ISO/IEC 23091-2_2019 subclause 8.3
/// Matrix coefficients according to ITU-T H.273.
/// </summary>
public enum CicpMatrixCoefficients : byte
{
@ -91,6 +91,21 @@ public enum CicpMatrixCoefficients : byte
/// Rec. ITU-R BT.2100-2 ICtCp
/// </summary>
ICtCp = 14,
/// <summary>
/// IPT-C2.
/// </summary>
IptC2 = 15,
/// <summary>
/// Reversible YCgCo with two additional encoded bits.
/// </summary>
YCgCoRe = 16,
/// <summary>
/// Reversible YCgCo with one additional encoded bit.
/// </summary>
YCgCoRo = 17,
}
#pragma warning restore CA1707 // Underscores in enum members

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

@ -522,9 +522,15 @@ public class Av1YuvConverterTests
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Bt709)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Identity)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.SmpteYCgCo)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.IptC2)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.Bt709)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.Identity)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.SmpteYCgCo)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.IptC2)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo)]
public void HighBitDepthRoundTrip(int bitDepth, int matrixCoefficients)
{
// Assign
@ -556,6 +562,203 @@ public class Av1YuvConverterTests
}
}
/// <summary>
/// Verifies the H.273 IPT-C2 matrices in both directions against independently calculated code values.
/// </summary>
[Fact]
public void IptC2MatchesKnownLinearTransferValuesInBothDirections()
{
// Assign
// The linear transfer characteristic isolates the two normative IPT-C2 matrices from transfer-curve error.
using Image<Rgb24> source = new(1, 1);
source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb24(150, 100, 50);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(
1,
1,
matrixCoefficients: ObuMatrixCoefficients.IptC2,
transferCharacteristics: ObuTransferCharacteristics.Linear);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
// Act
Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer);
// Assert
Assert.Equal(100, frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0]);
Assert.Equal(178, frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0]);
Assert.Equal(198, frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0]);
using Image<Rgb24> destination = new(1, 1);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame);
Assert.Equal(new Rgb24(150, 100, 51), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]);
}
/// <summary>
/// Verifies the reversible YCgCo lifting stages against known pure-red code values in both directions.
/// </summary>
/// <param name="bitDepth">The encoded AV1 bit depth.</param>
/// <param name="matrixCoefficients">The reversible YCgCo variant.</param>
/// <param name="expectedY">The expected encoded luma value.</param>
/// <param name="expectedU">The expected encoded Cg value.</param>
/// <param name="expectedV">The expected encoded Co value.</param>
[Theory]
[InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRe, 15, 97, 191)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe, 63, 385, 767)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe, 255, 1537, 3071)]
[InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRo, 31, 65, 255)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo, 127, 257, 1023)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo, 511, 1025, 4095)]
public void ReversibleYCgCoMatchesKnownPureRedValuesInBothDirections(
int bitDepth,
int matrixCoefficients,
int expectedY,
int expectedU,
int expectedV)
{
// Assign
// Pure red exercises positive odd Co and negative odd Cg. The expected samples come directly from
// the H.273 integer lifting equations at the logical RGB precision selected by each matrix code point.
using Image<Rgb48> source = new(1, 1);
source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb48(ushort.MaxValue, 0, 0);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(
1,
1,
matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients,
bitDepth: (Av1BitDepth)bitDepth);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
// Act
Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer);
// Assert
int actualY;
int actualU;
int actualV;
if ((Av1BitDepth)bitDepth == Av1BitDepth.EightBit)
{
actualY = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0];
actualU = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0];
actualV = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0];
}
else
{
actualY = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0)[0];
actualU = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 0, 0)[0];
actualV = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0)[0];
}
Assert.Equal(expectedY, actualY);
Assert.Equal(expectedU, actualU);
Assert.Equal(expectedV, actualV);
using Image<Rgb48> destination = new(1, 1);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame);
Assert.Equal(new Rgb48(ushort.MaxValue, 0, 0), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]);
}
/// <summary>
/// Verifies that limited-range reversible YCgCo applies range adjustment to RGB code values before lifting.
/// </summary>
/// <param name="bitDepth">The encoded AV1 bit depth.</param>
/// <param name="matrixCoefficients">The reversible YCgCo variant.</param>
/// <param name="expectedBlack">The expected black luma code value.</param>
/// <param name="expectedWhite">The expected white luma code value.</param>
[Theory]
[InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRe, 4, 59)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe, 16, 235)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe, 64, 940)]
[InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRo, 8, 118)]
[InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo, 32, 470)]
[InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo, 128, 1880)]
public void ReversibleYCgCoAppliesLimitedRangeBeforeLifting(
int bitDepth,
int matrixCoefficients,
int expectedBlack,
int expectedWhite)
{
// Assign
// Black and white have zero Cg and Co, exposing the RGB-domain range mapping without opponent-axis noise.
using Image<Rgb48> source = new(2, 1);
Span<Rgb48> sourcePixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
sourcePixels[0] = new Rgb48(0, 0, 0);
sourcePixels[1] = new Rgb48(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(
2,
1,
fullRange: false,
matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients,
bitDepth: (Av1BitDepth)bitDepth);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false);
// Act
Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer);
// Assert
int expectedChromaBias = 1 << (((Av1BitDepth)bitDepth).GetBitCount() - 1);
if ((Av1BitDepth)bitDepth == Av1BitDepth.EightBit)
{
Span<byte> y = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0);
Span<byte> u = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0);
Span<byte> v = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0);
Assert.Equal(expectedBlack, y[0]);
Assert.Equal(expectedWhite, y[1]);
Assert.Equal(expectedChromaBias, u[0]);
Assert.Equal(expectedChromaBias, u[1]);
Assert.Equal(expectedChromaBias, v[0]);
Assert.Equal(expectedChromaBias, v[1]);
}
else
{
Span<ushort> y = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0);
Span<ushort> u = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 0, 0);
Span<ushort> v = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0);
Assert.Equal(expectedBlack, y[0]);
Assert.Equal(expectedWhite, y[1]);
Assert.Equal(expectedChromaBias, u[0]);
Assert.Equal(expectedChromaBias, u[1]);
Assert.Equal(expectedChromaBias, v[0]);
Assert.Equal(expectedChromaBias, v[1]);
}
using Image<Rgb48> destination = new(2, 1);
Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame);
Span<Rgb48> destinationPixels = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0);
Assert.Equal(new Rgb48(0, 0, 0), destinationPixels[0]);
Assert.Equal(new Rgb48(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue), destinationPixels[1]);
}
/// <summary>
/// Verifies that reversible YCgCo rejects chroma subsampling in both conversion directions.
/// </summary>
/// <param name="matrixCoefficients">The reversible YCgCo variant.</param>
[Theory]
[InlineData(ObuMatrixCoefficients.YCgCoRe)]
[InlineData(ObuMatrixCoefficients.YCgCoRo)]
public void ReversibleYCgCoRequiresFullChroma(int matrixCoefficients)
{
// Assign
using Image<Rgb24> image = new(2, 2);
ObuSequenceHeader sequenceHeader = CreateSequenceHeader(
2,
2,
matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients,
colorFormat: Av1ColorFormat.Yuv420);
using Av1FrameBuffer<byte> frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false);
// Act and assert
Assert.Throws<InvalidImageContentException>(
() => Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer));
Assert.Throws<InvalidImageContentException>(
() => Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame));
}
/// <summary>
/// Verifies that every H.273 operator produces the same result in SIMD batches and the scalar row tail.
/// </summary>
@ -571,6 +774,9 @@ public class Av1YuvConverterTests
[InlineData(ObuMatrixCoefficients.ChromaticityDerivedConstantLuminance, ObuTransferCharacteristics.Bt709)]
[InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Smpte2084)]
[InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Hlg)]
[InlineData(ObuMatrixCoefficients.IptC2, ObuTransferCharacteristics.Bt709)]
[InlineData(ObuMatrixCoefficients.YCgCoRe, ObuTransferCharacteristics.Bt709)]
[InlineData(ObuMatrixCoefficients.YCgCoRo, ObuTransferCharacteristics.Bt709)]
public void ColorOperatorSimdBatchesMatchScalarTail(int matrixCoefficients, int transferCharacteristics)
{
const int width = 31;

Loading…
Cancel
Save