mirror of https://github.com/SixLabors/ImageSharp
25 changed files with 2263 additions and 996 deletions
@ -1,187 +0,0 @@ |
|||
// 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>
|
|||
/// <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, |
|||
float kb, |
|||
ObuTransferCharacteristics transferCharacteristics, |
|||
in Av1ConstantLuminanceScales constantLuminanceScales, |
|||
float lumaBias, |
|||
float lumaScale, |
|||
float chromaBias, |
|||
float chromaScale, |
|||
float encodedSampleMaximum, |
|||
float rgbBias, |
|||
float rgbScale, |
|||
float rgbSampleMaximum) |
|||
{ |
|||
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; |
|||
this.EncodedSampleMaximum = encodedSampleMaximum; |
|||
this.RgbBias = rgbBias; |
|||
this.RgbScale = rgbScale; |
|||
this.RgbSampleMaximum = rgbSampleMaximum; |
|||
} |
|||
|
|||
/// <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>
|
|||
/// 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>
|
|||
/// 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; } |
|||
} |
|||
@ -0,0 +1,498 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Color; |
|||
|
|||
/// <summary>
|
|||
/// Stores the resolved H.273 values shared by every scalar and SIMD lane.
|
|||
/// </summary>
|
|||
internal readonly struct HeifColorConversionParameters |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HeifColorConversionParameters"/> 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>
|
|||
/// <param name="lumaSampleMaximum">The largest encoded luma sample value.</param>
|
|||
/// <param name="chromaSampleMaximum">The largest encoded chroma 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 HeifColorConversionParameters( |
|||
float kr, |
|||
float kg, |
|||
float kb, |
|||
CicpTransferCharacteristics transferCharacteristics, |
|||
in HeifConstantLuminanceScales constantLuminanceScales, |
|||
float lumaBias, |
|||
float lumaScale, |
|||
float chromaBias, |
|||
float chromaScale, |
|||
float lumaSampleMaximum, |
|||
float chromaSampleMaximum, |
|||
float rgbBias, |
|||
float rgbScale, |
|||
float rgbSampleMaximum) |
|||
{ |
|||
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; |
|||
this.LumaSampleMaximum = lumaSampleMaximum; |
|||
this.ChromaSampleMaximum = chromaSampleMaximum; |
|||
this.RgbBias = rgbBias; |
|||
this.RgbScale = rgbScale; |
|||
this.RgbSampleMaximum = rgbSampleMaximum; |
|||
} |
|||
|
|||
/// <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 CicpTransferCharacteristics TransferCharacteristics { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the constant-luminance chroma scales.
|
|||
/// </summary>
|
|||
public HeifConstantLuminanceScales 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>
|
|||
/// Gets the largest encoded luma sample value.
|
|||
/// </summary>
|
|||
public float LumaSampleMaximum { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the largest encoded chroma sample value.
|
|||
/// </summary>
|
|||
public float ChromaSampleMaximum { 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>
|
|||
/// Resolves the H.273 matrix coefficients and sample ranges shared by HEVC and AV1 image items.
|
|||
/// </summary>
|
|||
/// <param name="colorPrimaries">The H.273 color-primary code point.</param>
|
|||
/// <param name="transferCharacteristics">The H.273 transfer-characteristic code point.</param>
|
|||
/// <param name="matrixCoefficients">The H.273 matrix-coefficient code point.</param>
|
|||
/// <param name="isFullRange">Whether encoded components use their complete numeric range.</param>
|
|||
/// <param name="lumaBitDepth">The encoded luma precision in bits.</param>
|
|||
/// <param name="chromaBitDepth">The encoded chroma precision in bits.</param>
|
|||
/// <param name="isMonochrome">Whether the image contains only luma samples.</param>
|
|||
/// <param name="hasFullResolutionChroma">Whether both chroma planes have luma resolution.</param>
|
|||
/// <param name="mode">The resolved conversion operation.</param>
|
|||
/// <returns>The immutable values used by each scalar and SIMD conversion lane.</returns>
|
|||
public static HeifColorConversionParameters Create( |
|||
CicpColorPrimaries colorPrimaries, |
|||
CicpTransferCharacteristics transferCharacteristics, |
|||
CicpMatrixCoefficients matrixCoefficients, |
|||
bool isFullRange, |
|||
int lumaBitDepth, |
|||
int chromaBitDepth, |
|||
bool isMonochrome, |
|||
bool hasFullResolutionChroma, |
|||
out HeifColorConversionMode mode) |
|||
{ |
|||
mode = HeifColorConversionMode.Coefficients; |
|||
float kr = 0F; |
|||
float kb = 0F; |
|||
|
|||
// H.273 assigns one closed arithmetic operation to each non-reserved matrix code point. Resolving the
|
|||
// operation once keeps both codec wrappers and every vector lane free from per-sample format dispatch.
|
|||
switch (matrixCoefficients) |
|||
{ |
|||
case CicpMatrixCoefficients.Identity: |
|||
mode = HeifColorConversionMode.Identity; |
|||
break; |
|||
case CicpMatrixCoefficients.ItuRBt709_6: |
|||
kr = 0.2126F; |
|||
kb = 0.0722F; |
|||
break; |
|||
case CicpMatrixCoefficients.Fcc47: |
|||
kr = 0.30F; |
|||
kb = 0.11F; |
|||
break; |
|||
case CicpMatrixCoefficients.ItuRBt601_7_625: |
|||
case CicpMatrixCoefficients.ItuRBt601_7_525: |
|||
case CicpMatrixCoefficients.Unspecified: |
|||
kr = 0.299F; |
|||
kb = 0.114F; |
|||
break; |
|||
case CicpMatrixCoefficients.SmpteSt240: |
|||
kr = 0.212F; |
|||
kb = 0.087F; |
|||
break; |
|||
case CicpMatrixCoefficients.YCgCo: |
|||
mode = HeifColorConversionMode.YCgCo; |
|||
break; |
|||
case CicpMatrixCoefficients.ItuRBt2020_2_Ncl: |
|||
kr = 0.2627F; |
|||
kb = 0.0593F; |
|||
break; |
|||
case CicpMatrixCoefficients.ItuRBt2020_2_Cl: |
|||
mode = HeifColorConversionMode.ConstantLuminance; |
|||
kr = 0.2627F; |
|||
kb = 0.0593F; |
|||
break; |
|||
case CicpMatrixCoefficients.SmpteSt2085: |
|||
mode = HeifColorConversionMode.Smpte2085; |
|||
break; |
|||
case CicpMatrixCoefficients.ChromaDerivedNcl: |
|||
GetChromaticityDerivedCoefficients(colorPrimaries, out kr, out kb); |
|||
break; |
|||
case CicpMatrixCoefficients.ChromaDerivedCl: |
|||
mode = HeifColorConversionMode.ConstantLuminance; |
|||
GetChromaticityDerivedCoefficients(colorPrimaries, out kr, out kb); |
|||
break; |
|||
case CicpMatrixCoefficients.ICtCp: |
|||
mode = HeifColorConversionMode.ICtCp; |
|||
break; |
|||
case CicpMatrixCoefficients.IptC2: |
|||
mode = HeifColorConversionMode.IptC2; |
|||
break; |
|||
case CicpMatrixCoefficients.YCgCoRe: |
|||
case CicpMatrixCoefficients.YCgCoRo: |
|||
mode = HeifColorConversionMode.YCgCoReversible; |
|||
break; |
|||
default: |
|||
throw new InvalidImageContentException($"The image declares reserved H.273 matrix coefficients '{(byte)matrixCoefficients}'."); |
|||
} |
|||
|
|||
bool requiresFullResolutionChroma = mode is HeifColorConversionMode.Identity or HeifColorConversionMode.YCgCoReversible; |
|||
if (requiresFullResolutionChroma && !isMonochrome && !hasFullResolutionChroma) |
|||
{ |
|||
throw new InvalidImageContentException($"H.273 matrix coefficients '{matrixCoefficients}' require 4:4:4 sampling."); |
|||
} |
|||
|
|||
bool requiresEqualBitDepth = mode is HeifColorConversionMode.Identity or HeifColorConversionMode.YCgCoReversible; |
|||
if (requiresEqualBitDepth && !isMonochrome && lumaBitDepth != chromaBitDepth) |
|||
{ |
|||
throw new InvalidImageContentException($"H.273 matrix coefficients '{matrixCoefficients}' require equal component bit depths."); |
|||
} |
|||
|
|||
float kg = 1F - kr - kb; |
|||
int lumaDepthScale = 1 << (lumaBitDepth - 8); |
|||
int chromaDepthScale = 1 << (chromaBitDepth - 8); |
|||
float lumaSampleMaximum = (1 << lumaBitDepth) - 1; |
|||
float chromaSampleMaximum = (1 << chromaBitDepth) - 1; |
|||
float chromaBias = 128F * chromaDepthScale; |
|||
float lumaBias = isFullRange ? 0F : 16F * lumaDepthScale; |
|||
float lumaScale = isFullRange ? lumaSampleMaximum : 219F * lumaDepthScale; |
|||
|
|||
// Limited-range YCgCo first range-adjusts RGB through the luma range, so its difference components use
|
|||
// 219 codes. Conventional YCbCr uses the independently specified 224-code chroma excursion.
|
|||
float chromaScale = isFullRange |
|||
? chromaSampleMaximum |
|||
: (mode == HeifColorConversionMode.YCgCo ? 219F : 224F) * chromaDepthScale; |
|||
|
|||
float rgbBias = 0F; |
|||
float rgbScale = 1F; |
|||
float rgbSampleMaximum = 1F; |
|||
if (mode == HeifColorConversionMode.YCgCoReversible) |
|||
{ |
|||
int bitOffset = matrixCoefficients == CicpMatrixCoefficients.YCgCoRe ? 2 : 1; |
|||
int rgbBitDepth = lumaBitDepth - bitOffset; |
|||
float rgbDepthScale = MathF.ScaleB(1F, rgbBitDepth - 8); |
|||
rgbSampleMaximum = (1 << rgbBitDepth) - 1; |
|||
rgbBias = isFullRange ? 0F : 16F * rgbDepthScale; |
|||
rgbScale = isFullRange ? rgbSampleMaximum : 219F * rgbDepthScale; |
|||
|
|||
// The reversible lifting transform works on integer code values. It performs RGB range adjustment
|
|||
// internally, while the row traversal normalizes all three encoded components to their full code range.
|
|||
lumaBias = 0F; |
|||
lumaScale = lumaSampleMaximum; |
|||
chromaScale = chromaSampleMaximum; |
|||
} |
|||
|
|||
HeifConstantLuminanceScales constantLuminanceScales = mode == HeifColorConversionMode.ConstantLuminance |
|||
? new HeifConstantLuminanceScales(transferCharacteristics, kr, kb) |
|||
: default; |
|||
|
|||
return new HeifColorConversionParameters( |
|||
kr, |
|||
kg, |
|||
kb, |
|||
transferCharacteristics, |
|||
in constantLuminanceScales, |
|||
lumaBias, |
|||
lumaScale, |
|||
chromaBias, |
|||
chromaScale, |
|||
lumaSampleMaximum, |
|||
chromaSampleMaximum, |
|||
rgbBias, |
|||
rgbScale, |
|||
rgbSampleMaximum); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes the luma coefficients defined by an H.273 primary-chromaticity code point.
|
|||
/// </summary>
|
|||
/// <param name="colorPrimaries">The H.273 color-primary code point.</param>
|
|||
/// <param name="kr">The resulting red luma coefficient.</param>
|
|||
/// <param name="kb">The resulting blue luma coefficient.</param>
|
|||
private static void GetChromaticityDerivedCoefficients(CicpColorPrimaries colorPrimaries, out float kr, out float kb) |
|||
{ |
|||
float redX; |
|||
float redY; |
|||
float greenX; |
|||
float greenY; |
|||
float blueX; |
|||
float blueY; |
|||
float whiteX; |
|||
float whiteY; |
|||
|
|||
switch (colorPrimaries) |
|||
{ |
|||
case CicpColorPrimaries.ItuRBt470_6M: |
|||
redX = 0.67F; |
|||
redY = 0.33F; |
|||
greenX = 0.21F; |
|||
greenY = 0.71F; |
|||
blueX = 0.14F; |
|||
blueY = 0.08F; |
|||
whiteX = 0.310F; |
|||
whiteY = 0.316F; |
|||
break; |
|||
case CicpColorPrimaries.ItuRBt601_7_625: |
|||
redX = 0.64F; |
|||
redY = 0.33F; |
|||
greenX = 0.29F; |
|||
greenY = 0.60F; |
|||
blueX = 0.15F; |
|||
blueY = 0.06F; |
|||
whiteX = 0.3127F; |
|||
whiteY = 0.3290F; |
|||
break; |
|||
case CicpColorPrimaries.ItuRBt601_7_525: |
|||
case CicpColorPrimaries.SmpteSt240: |
|||
redX = 0.630F; |
|||
redY = 0.340F; |
|||
greenX = 0.310F; |
|||
greenY = 0.595F; |
|||
blueX = 0.155F; |
|||
blueY = 0.070F; |
|||
whiteX = 0.3127F; |
|||
whiteY = 0.3290F; |
|||
break; |
|||
case CicpColorPrimaries.GenericFilm: |
|||
redX = 0.681F; |
|||
redY = 0.319F; |
|||
greenX = 0.243F; |
|||
greenY = 0.692F; |
|||
blueX = 0.145F; |
|||
blueY = 0.049F; |
|||
whiteX = 0.310F; |
|||
whiteY = 0.316F; |
|||
break; |
|||
case CicpColorPrimaries.ItuRBt2020_2: |
|||
redX = 0.708F; |
|||
redY = 0.292F; |
|||
greenX = 0.170F; |
|||
greenY = 0.797F; |
|||
blueX = 0.131F; |
|||
blueY = 0.046F; |
|||
whiteX = 0.3127F; |
|||
whiteY = 0.3290F; |
|||
break; |
|||
case CicpColorPrimaries.SmpteSt428_1: |
|||
redX = 1F; |
|||
redY = 0F; |
|||
greenX = 0F; |
|||
greenY = 1F; |
|||
blueX = 0F; |
|||
blueY = 0F; |
|||
whiteX = 1F / 3F; |
|||
whiteY = 1F / 3F; |
|||
break; |
|||
case CicpColorPrimaries.SmpteRp431_2: |
|||
redX = 0.680F; |
|||
redY = 0.320F; |
|||
greenX = 0.265F; |
|||
greenY = 0.690F; |
|||
blueX = 0.150F; |
|||
blueY = 0.060F; |
|||
whiteX = 0.314F; |
|||
whiteY = 0.351F; |
|||
break; |
|||
case CicpColorPrimaries.SmpteEg432_1: |
|||
redX = 0.680F; |
|||
redY = 0.320F; |
|||
greenX = 0.265F; |
|||
greenY = 0.690F; |
|||
blueX = 0.150F; |
|||
blueY = 0.060F; |
|||
whiteX = 0.3127F; |
|||
whiteY = 0.3290F; |
|||
break; |
|||
case CicpColorPrimaries.EbuTech3213E: |
|||
redX = 0.630F; |
|||
redY = 0.340F; |
|||
greenX = 0.295F; |
|||
greenY = 0.605F; |
|||
blueX = 0.155F; |
|||
blueY = 0.077F; |
|||
whiteX = 0.3127F; |
|||
whiteY = 0.3290F; |
|||
break; |
|||
default: |
|||
// Unspecified primaries cannot define a chromaticity-derived matrix. The established libavif
|
|||
// behavior supplies BT.709/D65 so the image has one deterministic interpretation.
|
|||
redX = 0.64F; |
|||
redY = 0.33F; |
|||
greenX = 0.30F; |
|||
greenY = 0.60F; |
|||
blueX = 0.15F; |
|||
blueY = 0.06F; |
|||
whiteX = 0.3127F; |
|||
whiteY = 0.3290F; |
|||
break; |
|||
} |
|||
|
|||
float redZ = 1F - (redX + redY); |
|||
float greenZ = 1F - (greenX + greenY); |
|||
float blueZ = 1F - (blueX + blueY); |
|||
float whiteZ = 1F - (whiteX + whiteY); |
|||
|
|||
// H.273 equations 39 and 40 solve the RGB-to-XYZ primary matrix at the signaled white point. Expanding the
|
|||
// determinant avoids a general matrix inversion and gives both codec paths the same rounding sequence.
|
|||
float denominator = whiteY * |
|||
((redX * ((greenY * blueZ) - (blueY * greenZ))) + |
|||
(greenX * ((blueY * redZ) - (redY * blueZ))) + |
|||
(blueX * ((redY * greenZ) - (greenY * redZ)))); |
|||
|
|||
kr = (redY * |
|||
((whiteX * ((greenY * blueZ) - (blueY * greenZ))) + |
|||
(whiteY * ((blueX * greenZ) - (greenX * blueZ))) + |
|||
(whiteZ * ((greenX * blueY) - (blueX * greenY))))) / denominator; |
|||
|
|||
kb = (blueY * |
|||
((whiteX * ((redY * greenZ) - (greenY * redZ))) + |
|||
(whiteY * ((greenX * redZ) - (redX * greenZ))) + |
|||
(whiteZ * ((redX * greenY) - (greenX * redY))))) / denominator; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Stores the H.273 chroma normalization constants for constant-luminance conversion.
|
|||
/// </summary>
|
|||
internal readonly struct HeifConstantLuminanceScales |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HeifConstantLuminanceScales"/> 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 HeifConstantLuminanceScales(CicpTransferCharacteristics transferCharacteristics, float kr, float kb) |
|||
{ |
|||
this.NegativeBlue = HeifTransferFunctions.ToGamma(transferCharacteristics, 1F - kb); |
|||
this.PositiveBlue = 1F - HeifTransferFunctions.ToGamma(transferCharacteristics, kb); |
|||
this.NegativeRed = HeifTransferFunctions.ToGamma(transferCharacteristics, 1F - kr); |
|||
this.PositiveRed = 1F - HeifTransferFunctions.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; } |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Hevc; |
|||
|
|||
/// <summary>
|
|||
/// Identifies the location of a 4:2:0 chroma sample relative to its associated two-by-two luma sample region.
|
|||
/// </summary>
|
|||
internal enum HevcChromaSampleLocation : byte |
|||
{ |
|||
/// <summary>
|
|||
/// The chroma sample is horizontally co-sited with the left luma column and vertically centered.
|
|||
/// </summary>
|
|||
Left = 0, |
|||
|
|||
/// <summary>
|
|||
/// The chroma sample is horizontally and vertically centered.
|
|||
/// </summary>
|
|||
Center = 1, |
|||
|
|||
/// <summary>
|
|||
/// The chroma sample is co-sited with the top-left luma sample.
|
|||
/// </summary>
|
|||
TopLeft = 2, |
|||
|
|||
/// <summary>
|
|||
/// The chroma sample is horizontally centered and co-sited with the top luma row.
|
|||
/// </summary>
|
|||
Top = 3, |
|||
|
|||
/// <summary>
|
|||
/// The chroma sample is horizontally co-sited with the left luma column and vertically co-sited with the bottom luma row.
|
|||
/// </summary>
|
|||
BottomLeft = 4, |
|||
|
|||
/// <summary>
|
|||
/// The chroma sample is horizontally centered and vertically co-sited with the bottom luma row.
|
|||
/// </summary>
|
|||
Bottom = 5, |
|||
} |
|||
@ -0,0 +1,592 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Runtime.InteropServices; |
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Common.Helpers; |
|||
using SixLabors.ImageSharp.Formats.Heif.Color; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using static SixLabors.ImageSharp.Formats.Heif.Color.HeifColorConverterBase; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Hevc; |
|||
|
|||
/// <summary>
|
|||
/// Converts between reconstructed HEVC component planes and packed ImageSharp pixels.
|
|||
/// </summary>
|
|||
internal static class HevcYuvConverter |
|||
{ |
|||
/// <summary>
|
|||
/// The largest value represented by an eight-bit packed RGB component.
|
|||
/// </summary>
|
|||
private const float ByteMaximum = byte.MaxValue; |
|||
|
|||
/// <summary>
|
|||
/// The largest value represented by a 16-bit packed RGB component.
|
|||
/// </summary>
|
|||
private const float UShortMaximum = ushort.MaxValue; |
|||
|
|||
/// <summary>
|
|||
/// Gets the horizontal offset in half-luma-sample units for each HEVC 4:2:0 chroma-location code.
|
|||
/// </summary>
|
|||
private static ReadOnlySpan<byte> ChromaLocationX => [0, 1, 0, 1, 0, 1]; |
|||
|
|||
/// <summary>
|
|||
/// Gets the vertical offset in half-luma-sample units for each HEVC 4:2:0 chroma-location code.
|
|||
/// </summary>
|
|||
private static ReadOnlySpan<byte> ChromaLocationY => [1, 1, 0, 0, 2, 2]; |
|||
|
|||
/// <summary>
|
|||
/// Converts reconstructed HEVC component planes to packed pixels.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
|
|||
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
|
|||
/// <param name="picture">The reconstructed HEVC picture.</param>
|
|||
/// <param name="image">The destination image frame.</param>
|
|||
/// <param name="colorProfile">The effective H.273 color description.</param>
|
|||
/// <param name="chromaSampleLocation">The progressive-frame 4:2:0 chroma sample location.</param>
|
|||
public static void ConvertToRgb<TPixel>( |
|||
Configuration configuration, |
|||
HevcPictureBuffer picture, |
|||
ImageFrame<TPixel> image, |
|||
CicpProfile colorProfile, |
|||
HevcChromaSampleLocation chromaSampleLocation) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
HeifColorConversionParameters parameters = GetConversionParameters(picture, colorProfile, out HeifColorConversionMode mode); |
|||
HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, picture.ChromaFormat == 0); |
|||
YuvToRgbRowConverter<TPixel> converter = new(configuration, picture, image, colorConverter, chromaSampleLocation); |
|||
using IMemoryOwner<float> scratchOwner = configuration.MemoryAllocator.Allocate<float>(converter.BufferLength); |
|||
Span<float> scratch = scratchOwner.GetSpan(); |
|||
|
|||
if (converter.UsesBytePacking) |
|||
{ |
|||
using IMemoryOwner<TPixel> proxyOwner = configuration.MemoryAllocator.Allocate<TPixel>(image.Width + 3); |
|||
Span<TPixel> proxy = proxyOwner.GetSpan()[..(image.Width + 3)]; |
|||
for (int y = 0; y < image.Height; y++) |
|||
{ |
|||
converter.Convert(y, scratch, proxy); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
for (int y = 0; y < image.Height; y++) |
|||
{ |
|||
converter.Convert(y, scratch, Span<TPixel>.Empty); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts packed pixels to the configured HEVC component planes.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The source pixel type.</typeparam>
|
|||
/// <param name="configuration">The configuration used for allocation and pixel conversion.</param>
|
|||
/// <param name="image">The source image frame.</param>
|
|||
/// <param name="picture">The destination HEVC picture.</param>
|
|||
/// <param name="colorProfile">The H.273 color description to encode.</param>
|
|||
/// <param name="chromaSampleLocation">The progressive-frame 4:2:0 chroma sample location.</param>
|
|||
public static void ConvertFromRgb<TPixel>( |
|||
Configuration configuration, |
|||
ImageFrame<TPixel> image, |
|||
HevcPictureBuffer picture, |
|||
CicpProfile colorProfile, |
|||
HevcChromaSampleLocation chromaSampleLocation) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
HeifColorConversionParameters parameters = GetConversionParameters(picture, colorProfile, out HeifColorConversionMode mode); |
|||
HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, picture.ChromaFormat == 0); |
|||
RgbToYuvRowConverter<TPixel> converter = new(configuration, picture, image, colorConverter, chromaSampleLocation, in parameters); |
|||
using IMemoryOwner<float> componentOwner = configuration.MemoryAllocator.Allocate<float>(converter.ComponentBufferLength); |
|||
Span<float> components = componentOwner.GetSpan(); |
|||
|
|||
if (converter.UsesByteInput) |
|||
{ |
|||
converter.Convert(Span<Rgb48>.Empty, components); |
|||
return; |
|||
} |
|||
|
|||
using IMemoryOwner<Rgb48> packedOwner = configuration.MemoryAllocator.Allocate<Rgb48>(image.Width); |
|||
converter.Convert(packedOwner.GetSpan()[..image.Width], components); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Resolves the shared H.273 conversion parameters for an HEVC picture.
|
|||
/// </summary>
|
|||
/// <param name="picture">The picture defining component precision and sampling.</param>
|
|||
/// <param name="colorProfile">The effective H.273 color description.</param>
|
|||
/// <param name="mode">The resolved color conversion operation.</param>
|
|||
/// <returns>The immutable scalar and SIMD conversion parameters.</returns>
|
|||
private static HeifColorConversionParameters GetConversionParameters( |
|||
HevcPictureBuffer picture, |
|||
CicpProfile colorProfile, |
|||
out HeifColorConversionMode mode) |
|||
=> HeifColorConversionParameters.Create( |
|||
colorProfile.ColorPrimaries, |
|||
colorProfile.TransferCharacteristics, |
|||
colorProfile.MatrixCoefficients, |
|||
colorProfile.FullRange, |
|||
picture.BitDepthLuma, |
|||
picture.ChromaFormat == 0 ? picture.BitDepthLuma : picture.BitDepthChroma, |
|||
picture.ChromaFormat == 0, |
|||
picture.ChromaFormat == 3, |
|||
out mode); |
|||
|
|||
/// <summary>
|
|||
/// Resolves the two chroma rows and quarter-sample weight surrounding a luma row.
|
|||
/// </summary>
|
|||
/// <param name="coordinate">The luma row coordinate.</param>
|
|||
/// <param name="subsampling">The vertical chroma subsampling shift.</param>
|
|||
/// <param name="position">The chroma offset in half-luma-sample units.</param>
|
|||
/// <param name="maximum">The last available chroma row.</param>
|
|||
/// <param name="lower">The lower chroma row.</param>
|
|||
/// <param name="upper">The upper chroma row.</param>
|
|||
/// <param name="upperWeight">The upper-row weight with a denominator of four.</param>
|
|||
private static void GetChromaCoordinates( |
|||
int coordinate, |
|||
int subsampling, |
|||
int position, |
|||
int maximum, |
|||
out int lower, |
|||
out int upper, |
|||
out int upperWeight) |
|||
{ |
|||
if (subsampling == 0) |
|||
{ |
|||
lower = coordinate; |
|||
upper = coordinate; |
|||
upperWeight = 0; |
|||
return; |
|||
} |
|||
|
|||
// Multiplying the source coordinate by four keeps all six HEVC 4:2:0 locations integral. Floor division
|
|||
// is required above the top edge because the first centered sample lies after the first luma row.
|
|||
int quarterCoordinate = (coordinate * 2) - position; |
|||
int unclampedLower = quarterCoordinate >= 0 ? quarterCoordinate >> 2 : -((-quarterCoordinate + 3) >> 2); |
|||
int fraction = quarterCoordinate - (unclampedLower * 4); |
|||
lower = Numerics.Clamp(unclampedLower, 0, maximum); |
|||
upper = Numerics.Clamp(unclampedLower + 1, 0, maximum); |
|||
upperWeight = lower == upper ? 0 : fraction; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Resolves the luma-relative chroma offsets for the selected plane layout.
|
|||
/// </summary>
|
|||
/// <param name="picture">The HEVC picture layout.</param>
|
|||
/// <param name="chromaSampleLocation">The signaled 4:2:0 location.</param>
|
|||
/// <param name="horizontalPosition">The horizontal offset in half-luma-sample units.</param>
|
|||
/// <param name="verticalPosition">The vertical offset in half-luma-sample units.</param>
|
|||
private static void GetChromaPosition( |
|||
HevcPictureBuffer picture, |
|||
HevcChromaSampleLocation chromaSampleLocation, |
|||
out int horizontalPosition, |
|||
out int verticalPosition) |
|||
{ |
|||
if (picture.ChromaFormat != 1 || picture.SeparateColorPlane) |
|||
{ |
|||
horizontalPosition = 0; |
|||
verticalPosition = 0; |
|||
return; |
|||
} |
|||
|
|||
int location = (int)chromaSampleLocation; |
|||
horizontalPosition = ChromaLocationX[location]; |
|||
verticalPosition = ChromaLocationY[location]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts one reconstructed HEVC row using pooled planar and packed-pixel storage.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
|
|||
private readonly struct YuvToRgbRowConverter<TPixel> |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// The configuration used for packed-pixel conversion.
|
|||
/// </summary>
|
|||
private readonly Configuration configuration; |
|||
|
|||
/// <summary>
|
|||
/// The reconstructed HEVC picture.
|
|||
/// </summary>
|
|||
private readonly HevcPictureBuffer picture; |
|||
|
|||
/// <summary>
|
|||
/// The destination image frame.
|
|||
/// </summary>
|
|||
private readonly ImageFrame<TPixel> image; |
|||
|
|||
/// <summary>
|
|||
/// The selected H.273 color converter.
|
|||
/// </summary>
|
|||
private readonly HeifColorConverterBase colorConverter; |
|||
|
|||
/// <summary>
|
|||
/// The horizontal chroma offset in half-luma-sample units.
|
|||
/// </summary>
|
|||
private readonly int horizontalPosition; |
|||
|
|||
/// <summary>
|
|||
/// The vertical chroma offset in half-luma-sample units.
|
|||
/// </summary>
|
|||
private readonly int verticalPosition; |
|||
|
|||
/// <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}"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration used for pixel conversion.</param>
|
|||
/// <param name="picture">The reconstructed HEVC picture.</param>
|
|||
/// <param name="image">The destination image frame.</param>
|
|||
/// <param name="colorConverter">The selected H.273 color converter.</param>
|
|||
/// <param name="chromaSampleLocation">The progressive-frame 4:2:0 chroma sample location.</param>
|
|||
public YuvToRgbRowConverter( |
|||
Configuration configuration, |
|||
HevcPictureBuffer picture, |
|||
ImageFrame<TPixel> image, |
|||
HeifColorConverterBase colorConverter, |
|||
HevcChromaSampleLocation chromaSampleLocation) |
|||
{ |
|||
this.configuration = configuration; |
|||
this.picture = picture; |
|||
this.image = image; |
|||
this.colorConverter = colorConverter; |
|||
this.subX = picture.GetSubsamplingX(HevcPlane.Cb); |
|||
this.subY = picture.GetSubsamplingY(HevcPlane.Cb); |
|||
GetChromaPosition(picture, chromaSampleLocation, out this.horizontalPosition, out this.verticalPosition); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the destination uses JPEG-compatible eight-bit RGB plane packing.
|
|||
/// </summary>
|
|||
public bool UsesBytePacking => this.picture.BitDepthLuma == 8 && (this.picture.ChromaFormat == 0 || this.picture.BitDepthChroma == 8); |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of float elements required by the reusable row buffer.
|
|||
/// </summary>
|
|||
public int BufferLength |
|||
{ |
|||
get |
|||
{ |
|||
int componentRowCount = this.picture.ChromaFormat == 0 ? 3 : 5; |
|||
int packedRowCount = this.UsesBytePacking ? 1 : 2; |
|||
return this.image.Width * (componentRowCount + packedRowCount); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts one reconstructed row to packed pixels.
|
|||
/// </summary>
|
|||
/// <param name="y">The zero-based luma row.</param>
|
|||
/// <param name="scratch">The reusable pooled row buffer.</param>
|
|||
/// <param name="proxy">The padded byte-packing destination when the image row has insufficient padding.</param>
|
|||
public void Convert(int y, Span<float> scratch, Span<TPixel> proxy) |
|||
{ |
|||
int width = this.image.Width; |
|||
Span<float> red = scratch[..width]; |
|||
Span<float> green = scratch.Slice(width, width); |
|||
Span<float> blue = scratch.Slice(width * 2, width); |
|||
ConvertSamplesToFloat<ushort, HeifUShortSampleLoader>(this.picture.GetRowSpan(HevcPlane.Y, y), red); |
|||
|
|||
int packedOffset = width * 3; |
|||
if (this.picture.ChromaFormat != 0) |
|||
{ |
|||
int chromaHeight = this.picture.GetHeight(HevcPlane.Cb); |
|||
GetChromaCoordinates(y, this.subY, this.verticalPosition, chromaHeight - 1, out int y0, out int y1, out int y1Weight); |
|||
ReadOnlySpan<ushort> cb0 = this.picture.GetRowSpan(HevcPlane.Cb, y0); |
|||
ReadOnlySpan<ushort> cb1 = this.picture.GetRowSpan(HevcPlane.Cb, y1); |
|||
ReadOnlySpan<ushort> cr0 = this.picture.GetRowSpan(HevcPlane.Cr, y0); |
|||
ReadOnlySpan<ushort> cr1 = this.picture.GetRowSpan(HevcPlane.Cr, y1); |
|||
Span<float> chroma0 = scratch.Slice(width * 3, width); |
|||
Span<float> chroma1 = scratch.Slice(width * 4, width); |
|||
bool isCenteredX = this.subX != 0 && this.horizontalPosition == 1; |
|||
ReconstructChromaRow<ushort, HeifUShortSampleLoader>(cb0, cb1, y1Weight, this.subX, isCenteredX, green, chroma0, chroma1); |
|||
ReconstructChromaRow<ushort, HeifUShortSampleLoader>(cr0, cr1, y1Weight, this.subX, isCenteredX, blue, chroma0, chroma1); |
|||
packedOffset = width * 5; |
|||
} |
|||
|
|||
this.colorConverter.ConvertToRgbInPlace(red, green, blue); |
|||
Span<TPixel> destination = this.image.PixelBuffer.DangerousGetRowSpan(y); |
|||
Span<float> packedStorage = scratch[packedOffset..]; |
|||
if (this.UsesBytePacking) |
|||
{ |
|||
// This is JPEG's planar packing contract. Existing pixel-specific SIMD packers therefore own the
|
|||
// final RGB-to-TPixel conversion instead of a HEVC-specific per-pixel loop.
|
|||
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); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
Span<Rgba64> packed = MemoryMarshal.Cast<float, Rgba64>(packedStorage)[..width]; |
|||
PackRgba64(red, green, blue, packed); |
|||
PixelOperations<TPixel>.Instance.FromRgba64(this.configuration, packed, destination); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts packed image rows to HEVC component planes using pooled planar storage.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The source pixel type.</typeparam>
|
|||
private readonly struct RgbToYuvRowConverter<TPixel> |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// The configuration used for packed-pixel conversion.
|
|||
/// </summary>
|
|||
private readonly Configuration configuration; |
|||
|
|||
/// <summary>
|
|||
/// The destination HEVC picture.
|
|||
/// </summary>
|
|||
private readonly HevcPictureBuffer picture; |
|||
|
|||
/// <summary>
|
|||
/// The source image frame.
|
|||
/// </summary>
|
|||
private readonly ImageFrame<TPixel> image; |
|||
|
|||
/// <summary>
|
|||
/// The selected H.273 color converter.
|
|||
/// </summary>
|
|||
private readonly HeifColorConverterBase colorConverter; |
|||
|
|||
/// <summary>
|
|||
/// The largest encoded luma sample value.
|
|||
/// </summary>
|
|||
private readonly float lumaMaximum; |
|||
|
|||
/// <summary>
|
|||
/// The largest encoded chroma sample value.
|
|||
/// </summary>
|
|||
private readonly float chromaMaximum; |
|||
|
|||
/// <summary>
|
|||
/// The horizontal chroma offset in half-luma-sample units.
|
|||
/// </summary>
|
|||
private readonly int horizontalPosition; |
|||
|
|||
/// <summary>
|
|||
/// The vertical chroma offset in half-luma-sample units.
|
|||
/// </summary>
|
|||
private readonly int verticalPosition; |
|||
|
|||
/// <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="RgbToYuvRowConverter{TPixel}"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration used for pixel conversion.</param>
|
|||
/// <param name="picture">The destination HEVC picture.</param>
|
|||
/// <param name="image">The source image frame.</param>
|
|||
/// <param name="colorConverter">The selected H.273 color converter.</param>
|
|||
/// <param name="chromaSampleLocation">The progressive-frame 4:2:0 chroma sample location.</param>
|
|||
/// <param name="parameters">The resolved H.273 component ranges.</param>
|
|||
public RgbToYuvRowConverter( |
|||
Configuration configuration, |
|||
HevcPictureBuffer picture, |
|||
ImageFrame<TPixel> image, |
|||
HeifColorConverterBase colorConverter, |
|||
HevcChromaSampleLocation chromaSampleLocation, |
|||
in HeifColorConversionParameters parameters) |
|||
{ |
|||
this.configuration = configuration; |
|||
this.picture = picture; |
|||
this.image = image; |
|||
this.colorConverter = colorConverter; |
|||
this.lumaMaximum = parameters.LumaSampleMaximum; |
|||
this.chromaMaximum = parameters.ChromaSampleMaximum; |
|||
this.subX = picture.GetSubsamplingX(HevcPlane.Cb); |
|||
this.subY = picture.GetSubsamplingY(HevcPlane.Cb); |
|||
GetChromaPosition(picture, chromaSampleLocation, out this.horizontalPosition, out this.verticalPosition); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether source pixels use JPEG-compatible eight-bit RGB plane unpacking.
|
|||
/// </summary>
|
|||
public bool UsesByteInput => this.picture.BitDepthLuma == 8 && (this.picture.ChromaFormat == 0 || this.picture.BitDepthChroma == 8); |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of float elements required by the reusable component buffer.
|
|||
/// </summary>
|
|||
public int ComponentBufferLength => this.image.Width * (this.subY == 0 ? 3 : 6); |
|||
|
|||
/// <summary>
|
|||
/// Converts every packed source row to the destination component planes.
|
|||
/// </summary>
|
|||
/// <param name="packed">The reusable high-bit-depth RGB staging row.</param>
|
|||
/// <param name="components">The reusable planar component buffer.</param>
|
|||
public void Convert(Span<Rgb48> packed, Span<float> components) |
|||
{ |
|||
int width = this.image.Width; |
|||
for (int y = 0; y < this.image.Height; y++) |
|||
{ |
|||
int slot = this.subY == 0 ? 0 : y & 1; |
|||
int offset = slot * width * 3; |
|||
Span<float> luma = components.Slice(offset, width); |
|||
Span<float> chromaBlue = components.Slice(offset + width, width); |
|||
Span<float> chromaRed = components.Slice(offset + (width * 2), width); |
|||
this.ConvertSourceRow(y, packed, luma, chromaBlue, chromaRed); |
|||
WriteSamples<ushort, HeifUShortSampleStorer>( |
|||
luma, |
|||
this.picture.GetRowSpan(HevcPlane.Y, y), |
|||
this.colorConverter.LumaScale, |
|||
this.colorConverter.LumaBias, |
|||
this.lumaMaximum); |
|||
|
|||
if (this.picture.ChromaFormat == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (this.subY == 0) |
|||
{ |
|||
this.WriteChromaRows(y, chromaBlue, chromaRed, Span<float>.Empty, Span<float>.Empty, 0F); |
|||
continue; |
|||
} |
|||
|
|||
bool isLastRow = y == this.image.Height - 1; |
|||
if (this.verticalPosition == 0 && (y & 1) == 0) |
|||
{ |
|||
this.WriteChromaRows(y >> 1, chromaBlue, chromaRed, Span<float>.Empty, Span<float>.Empty, 0F); |
|||
} |
|||
else if (this.verticalPosition == 1 && ((y & 1) != 0 || isLastRow)) |
|||
{ |
|||
if ((y & 1) != 0) |
|||
{ |
|||
Span<float> previousBlue = components.Slice(width, width); |
|||
Span<float> previousRed = components.Slice(width * 2, width); |
|||
this.WriteChromaRows(y >> 1, previousBlue, previousRed, chromaBlue, chromaRed, 0.5F); |
|||
} |
|||
else |
|||
{ |
|||
this.WriteChromaRows(y >> 1, chromaBlue, chromaRed, Span<float>.Empty, Span<float>.Empty, 0F); |
|||
} |
|||
} |
|||
else if (this.verticalPosition == 2 && ((y & 1) != 0 || isLastRow)) |
|||
{ |
|||
this.WriteChromaRows(y >> 1, chromaBlue, chromaRed, Span<float>.Empty, Span<float>.Empty, 0F); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts one packed source row into normalized planar 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 or first component values.</param>
|
|||
/// <param name="chromaBlue">The destination blue-difference or second component values.</param>
|
|||
/// <param name="chromaRed">The destination red-difference or third component 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 (this.UsesByteInput) |
|||
{ |
|||
// JPEG's planar unpack contract reaches the existing pixel-specific SIMD implementation before
|
|||
// the closed H.273 operator transforms the rows in place.
|
|||
PixelOperations<TPixel>.Instance.UnpackIntoRgbPlanes(luma, chromaBlue, chromaRed, source); |
|||
this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, ByteMaximum); |
|||
return; |
|||
} |
|||
|
|||
PixelOperations<TPixel>.Instance.ToRgb48(this.configuration, source, packed); |
|||
DeinterleaveRgb48(packed, luma, chromaBlue, chromaRed); |
|||
this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, UShortMaximum); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Filters and writes one pair of chroma component rows.
|
|||
/// </summary>
|
|||
/// <param name="destinationY">The destination chroma row.</param>
|
|||
/// <param name="blue0">The first blue-difference source row.</param>
|
|||
/// <param name="red0">The first red-difference source row.</param>
|
|||
/// <param name="blue1">The optional second blue-difference source row.</param>
|
|||
/// <param name="red1">The optional second red-difference source row.</param>
|
|||
/// <param name="row1Weight">The second-row contribution.</param>
|
|||
private void WriteChromaRows( |
|||
int destinationY, |
|||
ReadOnlySpan<float> blue0, |
|||
ReadOnlySpan<float> red0, |
|||
ReadOnlySpan<float> blue1, |
|||
ReadOnlySpan<float> red1, |
|||
float row1Weight) |
|||
{ |
|||
Span<ushort> blueDestination = this.picture.GetRowSpan(HevcPlane.Cb, destinationY); |
|||
Span<ushort> redDestination = this.picture.GetRowSpan(HevcPlane.Cr, destinationY); |
|||
if (this.subX == 0) |
|||
{ |
|||
WriteSamples<ushort, HeifUShortSampleStorer>( |
|||
blue0, |
|||
blueDestination, |
|||
this.colorConverter.ChromaScale, |
|||
this.colorConverter.ChromaBias, |
|||
this.chromaMaximum); |
|||
|
|||
WriteSamples<ushort, HeifUShortSampleStorer>( |
|||
red0, |
|||
redDestination, |
|||
this.colorConverter.ChromaScale, |
|||
this.colorConverter.ChromaBias, |
|||
this.chromaMaximum); |
|||
|
|||
return; |
|||
} |
|||
|
|||
bool isCenteredX = this.horizontalPosition == 1; |
|||
WriteSubsampledSamples<ushort, HeifUShortSampleStorer>( |
|||
blue0, |
|||
blue1, |
|||
blueDestination, |
|||
isCenteredX, |
|||
row1Weight, |
|||
this.colorConverter.ChromaScale, |
|||
this.colorConverter.ChromaBias, |
|||
this.chromaMaximum); |
|||
|
|||
WriteSubsampledSamples<ushort, HeifUShortSampleStorer>( |
|||
red0, |
|||
red1, |
|||
redDestination, |
|||
isCenteredX, |
|||
row1Weight, |
|||
this.colorConverter.ChromaScale, |
|||
this.colorConverter.ChromaBias, |
|||
this.chromaMaximum); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,185 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.Formats.Heif.Hevc; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; |
|||
|
|||
/// <summary>
|
|||
/// Measures frame-wide HEVC YUV 4:2:0 color conversion in both directions.
|
|||
/// </summary>
|
|||
[MemoryDiagnoser(displayGenColumns: false)] |
|||
public class HevcColorConversionBenchmarks |
|||
{ |
|||
/// <summary>
|
|||
/// The benchmark frame width.
|
|||
/// </summary>
|
|||
private const int Width = 1920; |
|||
|
|||
/// <summary>
|
|||
/// The benchmark frame height.
|
|||
/// </summary>
|
|||
private const int Height = 1080; |
|||
|
|||
/// <summary>
|
|||
/// The eight-bit source RGB image.
|
|||
/// </summary>
|
|||
private Image<Rgba32> byteSource = null!; |
|||
|
|||
/// <summary>
|
|||
/// The eight-bit destination RGB image.
|
|||
/// </summary>
|
|||
private Image<Rgba32> byteDestination = null!; |
|||
|
|||
/// <summary>
|
|||
/// The high-bit-depth source RGB image.
|
|||
/// </summary>
|
|||
private Image<Rgb48> highBitDepthSource = null!; |
|||
|
|||
/// <summary>
|
|||
/// The high-bit-depth destination RGB image.
|
|||
/// </summary>
|
|||
private Image<Rgb48> highBitDepthDestination = null!; |
|||
|
|||
/// <summary>
|
|||
/// The reusable HEVC component planes.
|
|||
/// </summary>
|
|||
private HevcPictureBuffer picture = null!; |
|||
|
|||
/// <summary>
|
|||
/// The H.273 profile used by both conversion directions.
|
|||
/// </summary>
|
|||
private CicpProfile colorProfile = null!; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the encoded HEVC 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.byteSource = new Image<Rgba32>(Width, Height); |
|||
this.byteDestination = new Image<Rgba32>(Width, Height); |
|||
this.highBitDepthSource = new Image<Rgb48>(Width, Height); |
|||
this.highBitDepthDestination = new Image<Rgb48>(Width, Height); |
|||
for (int y = 0; y < Height; y++) |
|||
{ |
|||
Span<Rgba32> byteRow = this.byteSource.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); |
|||
Span<Rgb48> highBitDepthRow = this.highBitDepthSource.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); |
|||
for (int x = 0; x < Width; x++) |
|||
{ |
|||
// Relatively prime channel steps exercise the row-wide converter without introducing setup randomness.
|
|||
byteRow[x] = new Rgba32((byte)((x * 29) + (y * 11)), (byte)((x * 17) + (y * 31)), (byte)((x * 7) + (y * 43))); |
|||
highBitDepthRow[x] = new Rgb48( |
|||
(ushort)((x * 1879) + (y * 791)), |
|||
(ushort)((x * 977) + (y * 3251)), |
|||
(ushort)((x * 613) + (y * 4987))); |
|||
} |
|||
} |
|||
|
|||
this.colorProfile = new CicpProfile( |
|||
(byte)CicpColorPrimaries.ItuRBt709_6, |
|||
(byte)CicpTransferCharacteristics.ItuRBt709_6, |
|||
(byte)CicpMatrixCoefficients.ItuRBt709_6, |
|||
false); |
|||
|
|||
this.picture = new HevcPictureBuffer(Configuration.Default, Width, Height, this.BitDepth, this.BitDepth, 1, false); |
|||
if (this.BitDepth == 8) |
|||
{ |
|||
HevcYuvConverter.ConvertFromRgb( |
|||
Configuration.Default, |
|||
this.byteSource.Frames.RootFrame, |
|||
this.picture, |
|||
this.colorProfile, |
|||
HevcChromaSampleLocation.Left); |
|||
} |
|||
else |
|||
{ |
|||
HevcYuvConverter.ConvertFromRgb( |
|||
Configuration.Default, |
|||
this.highBitDepthSource.Frames.RootFrame, |
|||
this.picture, |
|||
this.colorProfile, |
|||
HevcChromaSampleLocation.Left); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Releases the benchmark images and component planes.
|
|||
/// </summary>
|
|||
[GlobalCleanup] |
|||
public void Cleanup() |
|||
{ |
|||
this.picture.Dispose(); |
|||
this.highBitDepthDestination.Dispose(); |
|||
this.highBitDepthSource.Dispose(); |
|||
this.byteDestination.Dispose(); |
|||
this.byteSource.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Measures full-frame YUV-to-RGB conversion, including chroma reconstruction and packed-pixel conversion.
|
|||
/// </summary>
|
|||
/// <returns>A converted component that keeps the frame result observable.</returns>
|
|||
[Benchmark] |
|||
public ushort ConvertToRgb() |
|||
{ |
|||
if (this.BitDepth == 8) |
|||
{ |
|||
HevcYuvConverter.ConvertToRgb( |
|||
Configuration.Default, |
|||
this.picture, |
|||
this.byteDestination.Frames.RootFrame, |
|||
this.colorProfile, |
|||
HevcChromaSampleLocation.Left); |
|||
|
|||
return this.byteDestination[Width - 1, Height - 1].R; |
|||
} |
|||
|
|||
HevcYuvConverter.ConvertToRgb( |
|||
Configuration.Default, |
|||
this.picture, |
|||
this.highBitDepthDestination.Frames.RootFrame, |
|||
this.colorProfile, |
|||
HevcChromaSampleLocation.Left); |
|||
|
|||
return this.highBitDepthDestination[Width - 1, Height - 1].R; |
|||
} |
|||
|
|||
/// <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 ushort ConvertFromRgb() |
|||
{ |
|||
if (this.BitDepth == 8) |
|||
{ |
|||
HevcYuvConverter.ConvertFromRgb( |
|||
Configuration.Default, |
|||
this.byteSource.Frames.RootFrame, |
|||
this.picture, |
|||
this.colorProfile, |
|||
HevcChromaSampleLocation.Left); |
|||
} |
|||
else |
|||
{ |
|||
HevcYuvConverter.ConvertFromRgb( |
|||
Configuration.Default, |
|||
this.highBitDepthSource.Frames.RootFrame, |
|||
this.picture, |
|||
this.colorProfile, |
|||
HevcChromaSampleLocation.Left); |
|||
} |
|||
|
|||
return this.picture.GetRowSpan(HevcPlane.Y, Height - 1)[Width - 1]; |
|||
} |
|||
} |
|||
@ -0,0 +1,303 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.Formats.Heif.Hevc; |
|||
using SixLabors.ImageSharp.Metadata.Profiles.Cicp; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Hevc; |
|||
|
|||
/// <summary>
|
|||
/// Verifies the shared SIMD-first HEVC component conversion pipeline in both directions.
|
|||
/// </summary>
|
|||
[Trait("Format", "Heic")] |
|||
public class HevcYuvConverterTests |
|||
{ |
|||
/// <summary>
|
|||
/// Verifies that the identity matrix preserves every eight-bit RGB component through the HEVC GBR plane order.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void IdentityRoundTripIsExact() |
|||
{ |
|||
const int width = 19; |
|||
const int height = 7; |
|||
CicpProfile profile = CreateProfile(CicpMatrixCoefficients.Identity, true); |
|||
using Image<Rgba32> source = new(width, height); |
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); |
|||
for (int x = 0; x < width; x++) |
|||
{ |
|||
row[x] = new Rgba32((byte)((x * 17) + y), (byte)((y * 31) + x), (byte)((x * 7) + (y * 13))); |
|||
} |
|||
} |
|||
|
|||
using HevcPictureBuffer picture = new(Configuration.Default, width, height, 8, 8, 3, false); |
|||
HevcYuvConverter.ConvertFromRgb( |
|||
Configuration.Default, |
|||
source.Frames.RootFrame, |
|||
picture, |
|||
profile, |
|||
HevcChromaSampleLocation.Left); |
|||
|
|||
using Image<Rgba32> destination = new(width, height); |
|||
HevcYuvConverter.ConvertToRgb( |
|||
Configuration.Default, |
|||
picture, |
|||
destination.Frames.RootFrame, |
|||
profile, |
|||
HevcChromaSampleLocation.Left); |
|||
|
|||
for (int y = 0; y < height; y++) |
|||
{ |
|||
ReadOnlySpan<Rgba32> expected = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); |
|||
ReadOnlySpan<Rgba32> actual = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); |
|||
Assert.True(expected.SequenceEqual(actual)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that every exposed precision and plane layout executes both conversion directions on an odd-sized image.
|
|||
/// </summary>
|
|||
/// <param name="bitDepth">The encoded component precision.</param>
|
|||
/// <param name="chromaFormat">The HEVC chroma-format identifier.</param>
|
|||
/// <param name="fullRange">Whether components use their complete numeric range.</param>
|
|||
[Theory] |
|||
[InlineData(8, 0, true)] |
|||
[InlineData(8, 1, false)] |
|||
[InlineData(10, 2, true)] |
|||
[InlineData(12, 3, false)] |
|||
public void ConstantColorRoundTripsEveryPrecisionAndPlaneLayout(int bitDepth, byte chromaFormat, bool fullRange) |
|||
{ |
|||
const int width = 17; |
|||
const int height = 9; |
|||
Rgba64 expected = new(38550, 25700, 51400, ushort.MaxValue); |
|||
CicpProfile profile = CreateProfile(CicpMatrixCoefficients.ItuRBt709_6, fullRange); |
|||
using Image<Rgba64> source = new(width, height, expected); |
|||
using HevcPictureBuffer picture = new(Configuration.Default, width, height, bitDepth, bitDepth, chromaFormat, false); |
|||
|
|||
HevcYuvConverter.ConvertFromRgb( |
|||
Configuration.Default, |
|||
source.Frames.RootFrame, |
|||
picture, |
|||
profile, |
|||
HevcChromaSampleLocation.Left); |
|||
|
|||
using Image<Rgba64> destination = new(width, height); |
|||
HevcYuvConverter.ConvertToRgb( |
|||
Configuration.Default, |
|||
picture, |
|||
destination.Frames.RootFrame, |
|||
profile, |
|||
HevcChromaSampleLocation.Left); |
|||
|
|||
int tolerance = bitDepth switch |
|||
{ |
|||
8 => 800, |
|||
10 => 240, |
|||
_ => 80, |
|||
}; |
|||
|
|||
foreach (Rgba64 actual in destination.Frames.RootFrame.PixelBuffer.DangerousGetSingleSpan()) |
|||
{ |
|||
if (chromaFormat == 0) |
|||
{ |
|||
// A monochrome picture retains only luma. The decoded channels must therefore agree, while
|
|||
// comparing them with the chromatic source would incorrectly require discarded color to survive.
|
|||
Assert.InRange(Math.Abs(actual.R - actual.G), 0, tolerance); |
|||
Assert.InRange(Math.Abs(actual.R - actual.B), 0, tolerance); |
|||
Assert.Equal(ushort.MaxValue, actual.A); |
|||
continue; |
|||
} |
|||
|
|||
Assert.InRange(Math.Abs(actual.R - expected.R), 0, tolerance); |
|||
Assert.InRange(Math.Abs(actual.G - expected.G), 0, tolerance); |
|||
Assert.InRange(Math.Abs(actual.B - expected.B), 0, tolerance); |
|||
Assert.Equal(ushort.MaxValue, actual.A); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies the six HEVC 4:2:0 sample locations in both conversion directions.
|
|||
/// </summary>
|
|||
/// <param name="chromaSampleLocationValue">The signaled sample-location code point.</param>
|
|||
[Theory] |
|||
[InlineData(0)] |
|||
[InlineData(1)] |
|||
[InlineData(2)] |
|||
[InlineData(3)] |
|||
[InlineData(4)] |
|||
[InlineData(5)] |
|||
public void ConvertsEveryChromaSampleLocation(int chromaSampleLocationValue) |
|||
{ |
|||
const int width = 17; |
|||
const int height = 9; |
|||
Rgba64 expected = new(46260, 20560, 33410, ushort.MaxValue); |
|||
CicpProfile profile = CreateProfile(CicpMatrixCoefficients.ItuRBt709_6, true); |
|||
HevcChromaSampleLocation chromaSampleLocation = (HevcChromaSampleLocation)chromaSampleLocationValue; |
|||
using Image<Rgba64> source = new(width, height, expected); |
|||
using HevcPictureBuffer picture = new(Configuration.Default, width, height, 10, 10, 1, false); |
|||
|
|||
HevcYuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, picture, profile, chromaSampleLocation); |
|||
|
|||
using Image<Rgba64> destination = new(width, height); |
|||
HevcYuvConverter.ConvertToRgb(Configuration.Default, picture, destination.Frames.RootFrame, profile, chromaSampleLocation); |
|||
|
|||
Rgba64 actual = destination[width - 1, height - 1]; |
|||
Assert.InRange(Math.Abs(actual.R - expected.R), 0, 240); |
|||
Assert.InRange(Math.Abs(actual.G - expected.G), 0, 240); |
|||
Assert.InRange(Math.Abs(actual.B - expected.B), 0, 240); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that encoding selects the horizontal and vertical sample coordinates defined by all six location code points.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ChromaSampleLocationsSelectExpectedSourceCoordinatesWhenEncoding() |
|||
{ |
|||
CicpProfile profile = CreateProfile(CicpMatrixCoefficients.YCgCo, true); |
|||
using Image<Rgba32> source = new(2, 2); |
|||
source[0, 0] = new Rgba32(0, 0, 0); |
|||
source[1, 0] = new Rgba32(0, byte.MaxValue, 0); |
|||
source[0, 1] = new Rgba32(byte.MaxValue, 0, 0); |
|||
source[1, 1] = new Rgba32(0, 0, byte.MaxValue); |
|||
|
|||
// YCgCo maps the four source pixels to neutral, positive Cg, positive Co, and negative Co. Applying the
|
|||
// H.273 full-range scale after the location-specific point or average yields these exact encoded samples.
|
|||
ReadOnlySpan<ushort> expectedChromaBlue = [96, 128, 128, 192, 64, 64]; |
|||
ReadOnlySpan<ushort> expectedChromaRed = [192, 128, 128, 128, 255, 128]; |
|||
for (int location = 0; location < expectedChromaBlue.Length; location++) |
|||
{ |
|||
using HevcPictureBuffer picture = new(Configuration.Default, 2, 2, 8, 8, 1, false); |
|||
HevcYuvConverter.ConvertFromRgb( |
|||
Configuration.Default, |
|||
source.Frames.RootFrame, |
|||
picture, |
|||
profile, |
|||
(HevcChromaSampleLocation)location); |
|||
|
|||
Assert.Equal(expectedChromaBlue[location], picture.GetRowSpan(HevcPlane.Cb, 0)[0]); |
|||
Assert.Equal(expectedChromaRed[location], picture.GetRowSpan(HevcPlane.Cr, 0)[0]); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that decoding reconstructs chroma at the coordinates defined by all six location code points.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void ChromaSampleLocationsInterpolateExpectedCoordinatesWhenDecoding() |
|||
{ |
|||
CicpProfile profile = CreateProfile(CicpMatrixCoefficients.YCgCo, true); |
|||
|
|||
// At luma coordinate (1, 1), the six locations exercise centered and co-sited horizontal reconstruction
|
|||
// together with the quarter-sample weights for centered, top, and bottom vertical positions.
|
|||
ReadOnlySpan<byte> expectedRed = [96, 128, 128, 160, 64, 96]; |
|||
ReadOnlySpan<byte> expectedGreen = [160, 128, 128, 96, 192, 160]; |
|||
for (int location = 0; location < expectedRed.Length; location++) |
|||
{ |
|||
using HevcPictureBuffer picture = new(Configuration.Default, 4, 4, 8, 8, 1, false); |
|||
picture.Luma.DangerousGetSingleSpan().Fill(128); |
|||
picture.ChromaRed!.DangerousGetSingleSpan().Fill(128); |
|||
Span<ushort> topChromaBlue = picture.GetRowSpan(HevcPlane.Cb, 0); |
|||
Span<ushort> bottomChromaBlue = picture.GetRowSpan(HevcPlane.Cb, 1); |
|||
topChromaBlue[0] = 128; |
|||
topChromaBlue[1] = 255; |
|||
bottomChromaBlue[0] = 0; |
|||
bottomChromaBlue[1] = 128; |
|||
|
|||
using Image<Rgba32> destination = new(4, 4); |
|||
HevcYuvConverter.ConvertToRgb( |
|||
Configuration.Default, |
|||
picture, |
|||
destination.Frames.RootFrame, |
|||
profile, |
|||
(HevcChromaSampleLocation)location); |
|||
|
|||
Rgba32 actual = destination[1, 1]; |
|||
Assert.InRange(Math.Abs(actual.R - expectedRed[location]), 0, 1); |
|||
Assert.InRange(Math.Abs(actual.G - expectedGreen[location]), 0, 1); |
|||
Assert.InRange(Math.Abs(actual.B - expectedRed[location]), 0, 1); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies that independent luma and chroma precisions use their own H.273 code ranges.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void SupportsIndependentLumaAndChromaBitDepths() |
|||
{ |
|||
CicpProfile profile = CreateProfile(CicpMatrixCoefficients.ItuRBt709_6, false); |
|||
using Image<Rgba64> source = new(17, 5, new Rgba64(40000, 24000, 48000, ushort.MaxValue)); |
|||
using HevcPictureBuffer picture = new(Configuration.Default, 17, 5, 10, 12, 3, false); |
|||
|
|||
HevcYuvConverter.ConvertFromRgb( |
|||
Configuration.Default, |
|||
source.Frames.RootFrame, |
|||
picture, |
|||
profile, |
|||
HevcChromaSampleLocation.Left); |
|||
|
|||
foreach (ushort sample in picture.Luma.DangerousGetSingleSpan()) |
|||
{ |
|||
Assert.InRange(sample, (ushort)0, (ushort)1023); |
|||
} |
|||
|
|||
foreach (ushort sample in picture.ChromaBlue!.DangerousGetSingleSpan()) |
|||
{ |
|||
Assert.InRange(sample, (ushort)0, (ushort)4095); |
|||
} |
|||
|
|||
foreach (ushort sample in picture.ChromaRed!.DangerousGetSingleSpan()) |
|||
{ |
|||
Assert.InRange(sample, (ushort)0, (ushort)4095); |
|||
} |
|||
|
|||
using Image<Rgba64> destination = new(17, 5); |
|||
HevcYuvConverter.ConvertToRgb( |
|||
Configuration.Default, |
|||
picture, |
|||
destination.Frames.RootFrame, |
|||
profile, |
|||
HevcChromaSampleLocation.Left); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Verifies the exact limited-range endpoints written for ten-bit luma and chroma samples.
|
|||
/// </summary>
|
|||
[Fact] |
|||
public void LimitedRangeUsesExactTenBitEndpoints() |
|||
{ |
|||
CicpProfile profile = CreateProfile(CicpMatrixCoefficients.ItuRBt709_6, false); |
|||
using Image<Rgba64> source = new(2, 1); |
|||
source[0, 0] = new Rgba64(0, 0, 0, ushort.MaxValue); |
|||
source[1, 0] = new Rgba64(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, ushort.MaxValue); |
|||
using HevcPictureBuffer picture = new(Configuration.Default, 2, 1, 10, 10, 3, false); |
|||
|
|||
HevcYuvConverter.ConvertFromRgb( |
|||
Configuration.Default, |
|||
source.Frames.RootFrame, |
|||
picture, |
|||
profile, |
|||
HevcChromaSampleLocation.Left); |
|||
|
|||
Assert.Equal((ushort)64, picture.GetRowSpan(HevcPlane.Y, 0)[0]); |
|||
Assert.Equal((ushort)940, picture.GetRowSpan(HevcPlane.Y, 0)[1]); |
|||
Assert.Equal((ushort)512, picture.GetRowSpan(HevcPlane.Cb, 0)[0]); |
|||
Assert.Equal((ushort)512, picture.GetRowSpan(HevcPlane.Cb, 0)[1]); |
|||
Assert.Equal((ushort)512, picture.GetRowSpan(HevcPlane.Cr, 0)[0]); |
|||
Assert.Equal((ushort)512, picture.GetRowSpan(HevcPlane.Cr, 0)[1]); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a complete H.273 profile for a conversion test.
|
|||
/// </summary>
|
|||
/// <param name="matrixCoefficients">The matrix-coefficient code point.</param>
|
|||
/// <param name="fullRange">Whether components use their complete numeric range.</param>
|
|||
/// <returns>The configured profile.</returns>
|
|||
private static CicpProfile CreateProfile(CicpMatrixCoefficients matrixCoefficients, bool fullRange) |
|||
=> new( |
|||
(byte)CicpColorPrimaries.ItuRBt709_6, |
|||
(byte)CicpTransferCharacteristics.ItuRBt709_6, |
|||
(byte)matrixCoefficients, |
|||
fullRange); |
|||
} |
|||
Loading…
Reference in new issue