Browse Source

Add AV1 palette color coding

pull/2633/head
James Jackson-South 1 month ago
parent
commit
9451cd1ad0
  1. 1
      HEIF_IMPLEMENTATION_PLAN.md
  2. 169
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  3. 330
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  4. 135
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  5. 88
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

1
HEIF_IMPLEMENTATION_PLAN.md

@ -851,6 +851,7 @@ Encoder verification contract:
- [x] Filter-intra mode decision now runs after ordinary luma modes in current-libaom order, evaluates all five recursive predictors, and refines each predictor across every legal 8x8 transform in transform-enum order. Strictly-better replacement preserves ordinary-mode and filter-mode tie order. Each filter prediction and its source residual are prepared once and reused across transform candidates, avoiding repeated recursive prediction while retaining SIMD-first predictor and subtraction operators. The stack cost is 192 bytes for eight-bit samples or 256 bytes for high-bit-depth samples; no allocator owner or managed buffer was added. Fifteen production cases force every filter mode at 8, 10, and 12 bits and prove retained filter syntax, zero-residual reconstruction, and the DCT-DCT equal-cost transform tie. The decoded-frame MD5 values selected by this checkpoint are `d7d68803763b95827483f14515281d3a` for the 8x8 10-bit gradient, `3f7e34d44c65d7797ad26b5cd4c35bf4` for the 8x8 12-bit gradient, and `9985f05790d2c9f5f28723ef86d5b89b`, `2ba2f1d0fcfef60394a5175553c7cb8b`, and `6aa7a2ed0dbf76ad2ec0c222585272d0` for the odd 4:2:0, 4:2:2, and 4:4:4 gradients. The exact net11 Release rebuild remains at 1,005 warnings and zero errors, 18 focused filter-intra, predictor-reference, syntax-cost, and allocation cases pass, all 8,974 HEIF/AV1 tests pass, and current-main `aomdec` at `a40ed1ea9e4ecc3df58a5bccb76623f2c94ae727` accepts all 29 regenerated payloads. - [x] Filter-intra mode decision now runs after ordinary luma modes in current-libaom order, evaluates all five recursive predictors, and refines each predictor across every legal 8x8 transform in transform-enum order. Strictly-better replacement preserves ordinary-mode and filter-mode tie order. Each filter prediction and its source residual are prepared once and reused across transform candidates, avoiding repeated recursive prediction while retaining SIMD-first predictor and subtraction operators. The stack cost is 192 bytes for eight-bit samples or 256 bytes for high-bit-depth samples; no allocator owner or managed buffer was added. Fifteen production cases force every filter mode at 8, 10, and 12 bits and prove retained filter syntax, zero-residual reconstruction, and the DCT-DCT equal-cost transform tie. The decoded-frame MD5 values selected by this checkpoint are `d7d68803763b95827483f14515281d3a` for the 8x8 10-bit gradient, `3f7e34d44c65d7797ad26b5cd4c35bf4` for the 8x8 12-bit gradient, and `9985f05790d2c9f5f28723ef86d5b89b`, `2ba2f1d0fcfef60394a5175553c7cb8b`, and `6aa7a2ed0dbf76ad2ec0c222585272d0` for the odd 4:2:0, 4:2:2, and 4:4:4 gradients. The exact net11 Release rebuild remains at 1,005 warnings and zero errors, 18 focused filter-intra, predictor-reference, syntax-cost, and allocation cases pass, all 8,974 HEIF/AV1 tests pass, and current-main `aomdec` at `a40ed1ea9e4ecc3df58a5bccb76623f2c94ae727` accepts all 29 regenerated payloads.
- [x] Empty-transform block skip now compares the complete live rate of the two decoder-identical syntax choices after luma and every coded chroma plane have been selected. Current libaom forces all-intra blocks to non-skip; this encoder retains that behavior for every non-empty block and for equal-cost empty blocks, but emits block skip when its adapted context cost is strictly lower than non-skip plus all empty-transform coefficient costs. Costing and writing share the same above-and-left skip-context calculation, and the coefficient estimator returns after the transform-block-skip symbol without reading coefficient storage. This adds no allocation, copy, or persistent state. A focused adapted-CDF regression proves both outcomes through the production decision helper, the two production all-zero fixtures still prove the default real block path, the exact net11 Release rebuild remains at 1,005 warnings and zero errors, all 8,975 HEIF/AV1 tests pass, and current-main `aomdec` at `a40ed1ea9e4ecc3df58a5bccb76623f2c94ae727` accepts all 29 regenerated payloads. - [x] Empty-transform block skip now compares the complete live rate of the two decoder-identical syntax choices after luma and every coded chroma plane have been selected. Current libaom forces all-intra blocks to non-skip; this encoder retains that behavior for every non-empty block and for equal-cost empty blocks, but emits block skip when its adapted context cost is strictly lower than non-skip plus all empty-transform coefficient costs. Costing and writing share the same above-and-left skip-context calculation, and the coefficient estimator returns after the transform-block-skip symbol without reading coefficient storage. This adds no allocation, copy, or persistent state. A focused adapted-CDF regression proves both outcomes through the production decision helper, the two production all-zero fixtures still prove the default real block path, the exact net11 Release rebuild remains at 1,005 warnings and zero errors, all 8,975 HEIF/AV1 tests pass, and current-main `aomdec` at `a40ed1ea9e4ecc3df58a5bccb76623f2c94ae727` accepts all 29 regenerated payloads.
- [~] Palette entropy coding now mirrors current libaom's adaptive luma-mode, chroma-mode, palette-size, and spatial color-index distributions, together with its truncated-binary uniform code used by palette colors. The complete mutable palette probability graph is created once on first palette search or write, so the current palette-disabled frame path retains zero palette allocations. Three focused regressions cover every legal 2-through-8 color alphabet and every defined mode, size, and color-index context; all 1,928 entropy cases and all 8,978 HEIF/AV1 cases pass direct net11 Release VSTest. The exact Release rebuild remains at 1,005 warnings and zero errors. This checkpoint adds the exact entropy foundation only: palette candidate generation, retained color and index storage, mode decision, map tokenization, and production syntax remain incomplete, and no generated payload changed. - [~] Palette entropy coding now mirrors current libaom's adaptive luma-mode, chroma-mode, palette-size, and spatial color-index distributions, together with its truncated-binary uniform code used by palette colors. The complete mutable palette probability graph is created once on first palette search or write, so the current palette-disabled frame path retains zero palette allocations. Three focused regressions cover every legal 2-through-8 color alphabet and every defined mode, size, and color-index context; all 1,928 entropy cases and all 8,978 HEIF/AV1 cases pass direct net11 Release VSTest. The exact Release rebuild remains at 1,005 warnings and zero errors. This checkpoint adds the exact entropy foundation only: palette candidate generation, retained color and index storage, mode decision, map tokenization, and production syntax remain incomplete, and no generated payload changed.
- [~] Luma and chroma palette-color coding now matches current libaom's neighbor-cache flags, sorted delta representation, wrapped V-plane deltas, strict delta-versus-raw V selection, and fixed-point color-rate model at 8, 10, and 12 bits. Encoder costing and emission use only fixed stack spans, including explicitly initialized cache-membership state, and steady-state color costing allocates zero managed bytes. The decoder consumes the same bounded color-syntax primitive after the tile reader derives its neighbor cache, removing duplicated color parsing without changing retained palette ownership. Nine focused syntax, exact palette decode, constrained-allocation, truncation, presentation, and allocation cases pass; all 1,933 entropy cases and all 8,983 HEIF/AV1 cases pass direct net11 Release VSTest. The exact Release rebuild remains at 1,005 warnings and zero errors. Retained encoder palette colors, neighbor caches, color-index maps, candidate generation, and production palette selection remain incomplete, and the compact 8-byte frame mode entries were not enlarged.
- [x] The expanded checkpoint exposed a pre-existing transform-block test that asserted uninitialized pooled padding was zero. The test now initializes the complete physical luma plane with a sentinel and proves the block operation leaves both adjacent padding samples unchanged. The exact net11 Release rebuild remains at 1,005 baseline warnings and zero errors, the focused allocator-order set passes 30 of 30 cases, and the complete HEIF/AV1 namespace passes 8,859 of 8,859 direct VSTest cases with zero failures or skips. - [x] The expanded checkpoint exposed a pre-existing transform-block test that asserted uninitialized pooled padding was zero. The test now initializes the complete physical luma plane with a sentinel and proves the block operation leaves both adjacent padding samples unchanged. The exact net11 Release rebuild remains at 1,005 baseline warnings and zero errors, the focused allocator-order set passes 30 of 30 cases, and the complete HEIF/AV1 namespace passes 8,859 of 8,859 direct VSTest cases with zero failures or skips.
- [x] Combined-frame OBU output now counts the byte-aligned frame and tile-group headers, non-final tile-size fields, and owned tile payloads before emitting the OBU size. It retains only the small allocator-owned header scratch and writes each entropy-coded tile span directly from its detached owner, removing the second file-sized allocator rent and complete-payload copy. A 64 KiB regression proves exactly one sub-payload-sized byte rent with a balanced return and verifies the exact streamed tile tail; the existing two-tile round trip proves size-prefix and ordering parity. The focused writer and production-frame set passes 32 of 32 direct net11 VSTest cases, current-main `aomdec` accepts all 29 generated native-format payloads, and the complete HEIF/AV1 namespace passes 8,860 of 8,860 cases with zero failures or skips. - [x] Combined-frame OBU output now counts the byte-aligned frame and tile-group headers, non-final tile-size fields, and owned tile payloads before emitting the OBU size. It retains only the small allocator-owned header scratch and writes each entropy-coded tile span directly from its detached owner, removing the second file-sized allocator rent and complete-payload copy. A 64 KiB regression proves exactly one sub-payload-sized byte rent with a balanced return and verifies the exact streamed tile tail; the existing two-tile round trip proves size-prefix and ordering parity. The focused writer and production-frame set passes 32 of 32 direct net11 VSTest cases, current-main `aomdec` accepts all 29 generated native-format payloads, and the complete HEIF/AV1 namespace passes 8,860 of 8,860 cases with zero failures or skips.
- [x] Finalized fixed-block decisions now set the block-level transform-skip flag only when every retained luma and coded chroma transform has zero EOB, matching current libaom's conjunction of per-plane skip state. The previous always-false flag produced legal but redundant non-skip and zero-coefficient syntax. Monochrome and 4:2:0 regressions prove both branches from actual coefficient state; the focused decision and production-frame set passes 32 of 32 direct net11 VSTest cases. Current-main `aomdec` accepts all 29 regenerated payloads, the recorded decoded-frame MD5s are unchanged, and affected 16x16 constant 8-bit and 10-bit payloads are one byte smaller. The complete HEIF/AV1 namespace passes 8,862 of 8,862 cases with zero failures or skips. - [x] Finalized fixed-block decisions now set the block-level transform-skip flag only when every retained luma and coded chroma transform has zero EOB, matching current libaom's conjunction of per-plane skip state. The previous always-false flag produced legal but redundant non-skip and zero-coefficient syntax. Monochrome and 4:2:0 regressions prove both branches from actual coefficient state; the focused decision and production-frame set passes 32 of 32 direct net11 VSTest cases. Current-main `aomdec` accepts all 29 regenerated payloads, the recorded decoded-frame MD5s are unchanged, and affected 16x16 constant 8-bit and 10-bit payloads are one byte smaller. The complete HEIF/AV1 namespace passes 8,862 of 8,862 cases with zero failures or skips.

169
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs

@ -110,6 +110,141 @@ internal ref struct Av1SymbolDecoder
return (value << 1) - threshold + r.ReadLiteral(1); return (value << 1) - threshold + r.ReadLiteral(1);
} }
/// <summary>
/// Reads sorted luma palette colors using selections from the neighboring color cache.
/// </summary>
/// <param name="colorCache">The sorted unique colors inherited from eligible neighbors.</param>
/// <param name="paletteSize">The number of palette colors.</param>
/// <param name="bitDepth">The number of bits in each color sample.</param>
/// <param name="colors">The destination palette colors in prediction-index order.</param>
public void ReadPaletteYColors(
scoped ReadOnlySpan<ushort> colorCache,
int paletteSize,
int bitDepth,
scoped Span<ushort> colors)
{
Span<ushort> cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
int colorIndex = 0;
for (int i = 0; i < colorCache.Length && colorIndex < paletteSize; i++)
{
if (this.ReadLiteral(1) != 0)
{
cachedColors[colorIndex++] = colorCache[i];
}
}
if (colorIndex == paletteSize)
{
cachedColors[..paletteSize].CopyTo(colors);
return;
}
int cachedColorCount = colorIndex;
colors[colorIndex++] = (ushort)this.ReadLiteral(bitDepth);
if (colorIndex < paletteSize)
{
int bits = bitDepth - 3 + this.ReadLiteral(2);
int maximumColor = (1 << bitDepth) - 1;
int range = maximumColor - colors[colorIndex - 1];
for (; colorIndex < paletteSize; colorIndex++)
{
int delta = this.ReadLiteral(bits) + 1;
colors[colorIndex] = (ushort)Av1Math.Clip3(0, maximumColor, colors[colorIndex - 1] + delta);
range -= colors[colorIndex] - colors[colorIndex - 1];
bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range));
}
}
MergePaletteColors(colors, cachedColors, paletteSize, cachedColorCount);
}
/// <summary>
/// Reads shared chroma palette colors using cached U values and raw or delta-coded V values.
/// </summary>
/// <param name="colorCache">The sorted unique U colors inherited from eligible neighbors.</param>
/// <param name="paletteSize">The number of palette colors.</param>
/// <param name="bitDepth">The number of bits in each color sample.</param>
/// <param name="uColors">The destination U palette colors.</param>
/// <param name="vColors">The destination V palette colors paired with <paramref name="uColors"/>.</param>
public void ReadPaletteUvColors(
scoped ReadOnlySpan<ushort> colorCache,
int paletteSize,
int bitDepth,
scoped Span<ushort> uColors,
scoped Span<ushort> vColors)
{
Span<ushort> cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
int colorIndex = 0;
for (int i = 0; i < colorCache.Length && colorIndex < paletteSize; i++)
{
if (this.ReadLiteral(1) != 0)
{
cachedColors[colorIndex++] = colorCache[i];
}
}
if (colorIndex < paletteSize)
{
int cachedColorCount = colorIndex;
uColors[colorIndex++] = (ushort)this.ReadLiteral(bitDepth);
if (colorIndex < paletteSize)
{
int bits = bitDepth - 3 + this.ReadLiteral(2);
int maximumColor = (1 << bitDepth) - 1;
int range = (1 << bitDepth) - uColors[colorIndex - 1];
for (; colorIndex < paletteSize; colorIndex++)
{
int delta = this.ReadLiteral(bits);
uColors[colorIndex] = (ushort)Av1Math.Clip3(0, maximumColor, uColors[colorIndex - 1] + delta);
range -= uColors[colorIndex] - uColors[colorIndex - 1];
bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range));
}
}
MergePaletteColors(uColors, cachedColors, paletteSize, cachedColorCount);
}
else
{
cachedColors[..paletteSize].CopyTo(uColors);
}
if (this.ReadLiteral(1) != 0)
{
// V deltas wrap in the unsigned sample domain so complementary chroma colors remain compact.
int bits = bitDepth - 4 + this.ReadLiteral(2);
int sampleRange = 1 << bitDepth;
vColors[0] = (ushort)this.ReadLiteral(bitDepth);
for (int i = 1; i < paletteSize; i++)
{
int delta = this.ReadLiteral(bits);
if (delta != 0 && this.ReadLiteral(1) != 0)
{
delta = -delta;
}
int value = vColors[i - 1] + delta;
if (value < 0)
{
value += sampleRange;
}
if (value >= sampleRange)
{
value -= sampleRange;
}
vColors[i] = (ushort)value;
}
}
else
{
for (int i = 0; i < paletteSize; i++)
{
vColors[i] = (ushort)this.ReadLiteral(bitDepth);
}
}
}
/// <summary> /// <summary>
/// Reads a finite subexponential value recentered around a preceding value. /// Reads a finite subexponential value recentered around a preceding value.
/// </summary> /// </summary>
@ -158,6 +293,40 @@ internal ref struct Av1SymbolDecoder
return r.ReadSymbol(this.context.SgrProjectionRestoration) != 0; return r.ReadSymbol(this.context.SgrProjectionRestoration) != 0;
} }
/// <summary>
/// Merges selected cached colors with the sorted transmitted colors.
/// </summary>
/// <param name="colors">The transmitted colors beginning at <paramref name="cachedColorCount"/> and the merged output.</param>
/// <param name="cachedColors">The selected cached colors in ascending order.</param>
/// <param name="paletteSize">The total palette size.</param>
/// <param name="cachedColorCount">The number of selected cached colors.</param>
private static void MergePaletteColors(
Span<ushort> colors,
ReadOnlySpan<ushort> cachedColors,
int paletteSize,
int cachedColorCount)
{
if (cachedColorCount == 0)
{
return;
}
int cacheIndex = 0;
int transmittedIndex = cachedColorCount;
for (int i = 0; i < paletteSize; i++)
{
if (cacheIndex < cachedColorCount &&
(transmittedIndex >= paletteSize || cachedColors[cacheIndex] <= colors[transmittedIndex]))
{
colors[i] = cachedColors[cacheIndex++];
}
else
{
colors[i] = colors[transmittedIndex++];
}
}
}
/// <summary> /// <summary>
/// Reads a finite subexponential code from the tile entropy stream. /// Reads a finite subexponential code from the tile entropy stream.
/// </summary> /// </summary>

330
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs

@ -418,6 +418,167 @@ internal class Av1SymbolEncoder : IDisposable
w.WriteSymbol(colorOrderIndex, distribution); w.WriteSymbol(colorOrderIndex, distribution);
} }
/// <summary>
/// Gets the fixed-point rate of the luma palette colors.
/// </summary>
/// <param name="colorCache">The sorted unique colors inherited from eligible neighbors.</param>
/// <param name="colors">The sorted luma palette colors.</param>
/// <param name="bitDepth">The number of bits in each color sample.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public static int GetPaletteYColorCost(
ReadOnlySpan<ushort> colorCache,
ReadOnlySpan<ushort> colors,
int bitDepth)
{
Span<byte> cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2];
Span<ushort> uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
int uncachedColorCount = IndexColorCache(
colorCache,
colors,
cacheColorFound,
uncachedColors);
// Palette RD modeling charges every available cache flag even though emission can stop once all colors match.
int bitCount = colorCache.Length +
GetDeltaEncodedColorBitCount(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 1);
return Av1ProbabilityCost.GetLiteralCost(bitCount);
}
/// <summary>
/// Writes the luma palette colors using neighboring cache selections followed by sorted deltas.
/// </summary>
/// <param name="colorCache">The sorted unique colors inherited from eligible neighbors.</param>
/// <param name="colors">The sorted luma palette colors.</param>
/// <param name="bitDepth">The number of bits in each color sample.</param>
public void WritePaletteYColors(
ReadOnlySpan<ushort> colorCache,
ReadOnlySpan<ushort> colors,
int bitDepth)
{
Span<byte> cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2];
Span<ushort> uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
int uncachedColorCount = IndexColorCache(
colorCache,
colors,
cacheColorFound,
uncachedColors);
int cachedColorCount = 0;
for (int i = 0; i < colorCache.Length && cachedColorCount < colors.Length; i++)
{
byte found = cacheColorFound[i];
this.WriteLiteral(found, 1);
cachedColorCount += found;
}
this.WriteDeltaEncodedColors(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 1);
}
/// <summary>
/// Gets the fixed-point rate of the shared chroma palette colors.
/// </summary>
/// <param name="colorCache">The sorted unique U colors inherited from eligible neighbors.</param>
/// <param name="uColors">The sorted U palette colors.</param>
/// <param name="vColors">The V palette colors paired with <paramref name="uColors"/>.</param>
/// <param name="bitDepth">The number of bits in each color sample.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public static int GetPaletteUvColorCost(
ReadOnlySpan<ushort> colorCache,
ReadOnlySpan<ushort> uColors,
ReadOnlySpan<ushort> vColors,
int bitDepth)
{
Span<byte> cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2];
Span<ushort> uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
int uncachedColorCount = IndexColorCache(
colorCache,
uColors,
cacheColorFound,
uncachedColors);
// Palette RD modeling charges every available cache flag even though emission can stop once all colors match.
int bitCount = colorCache.Length +
GetDeltaEncodedColorBitCount(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 0);
int deltaBits = GetPaletteVDeltaBitCount(vColors, bitDepth, out int zeroCount, out int minimumBits);
int deltaBitCount = 2 + bitDepth + ((deltaBits + 1) * (vColors.Length - 1)) - zeroCount;
int rawBitCount = bitDepth * vColors.Length;
bitCount += 1 + Math.Min(deltaBitCount, rawBitCount);
return Av1ProbabilityCost.GetLiteralCost(bitCount);
}
/// <summary>
/// Writes the shared chroma palette colors using cached U values and the cheaper V representation.
/// </summary>
/// <param name="colorCache">The sorted unique U colors inherited from eligible neighbors.</param>
/// <param name="uColors">The sorted U palette colors.</param>
/// <param name="vColors">The V palette colors paired with <paramref name="uColors"/>.</param>
/// <param name="bitDepth">The number of bits in each color sample.</param>
public void WritePaletteUvColors(
ReadOnlySpan<ushort> colorCache,
ReadOnlySpan<ushort> uColors,
ReadOnlySpan<ushort> vColors,
int bitDepth)
{
Span<byte> cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2];
Span<ushort> uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
int uncachedColorCount = IndexColorCache(
colorCache,
uColors,
cacheColorFound,
uncachedColors);
int cachedColorCount = 0;
for (int i = 0; i < colorCache.Length && cachedColorCount < uColors.Length; i++)
{
byte found = cacheColorFound[i];
this.WriteLiteral(found, 1);
cachedColorCount += found;
}
this.WriteDeltaEncodedColors(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 0);
int deltaBits = GetPaletteVDeltaBitCount(vColors, bitDepth, out int zeroCount, out int minimumBits);
int deltaBitCount = 2 + bitDepth + ((deltaBits + 1) * (vColors.Length - 1)) - zeroCount;
int rawBitCount = bitDepth * vColors.Length;
bool useDelta = deltaBitCount < rawBitCount;
this.WriteLiteral(useDelta ? 1u : 0u, 1);
if (!useDelta)
{
for (int i = 0; i < vColors.Length; i++)
{
this.WriteLiteral(vColors[i], bitDepth);
}
return;
}
this.WriteLiteral((uint)(deltaBits - minimumBits), 2);
this.WriteLiteral(vColors[0], bitDepth);
int sampleRange = 1 << bitDepth;
for (int i = 1; i < vColors.Length; i++)
{
int signedDelta = vColors[i] - vColors[i - 1];
int delta = Math.Abs(signedDelta);
// Chroma wraps in its unsigned sample domain, so signal whichever circular direction has less magnitude.
if (delta <= sampleRange - delta)
{
this.WriteLiteral((uint)delta, deltaBits);
if (delta != 0)
{
this.WriteLiteral(signedDelta < 0 ? 1u : 0u, 1);
}
}
else
{
this.WriteLiteral((uint)(sampleRange - delta), deltaBits);
this.WriteLiteral(signedDelta < 0 ? 0u : 1u, 1);
}
}
}
/// <summary> /// <summary>
/// Writes the frame-local intra-block-copy flag. /// Writes the frame-local intra-block-copy flag.
/// </summary> /// </summary>
@ -1366,6 +1527,175 @@ internal class Av1SymbolEncoder : IDisposable
} }
} }
/// <summary>
/// Separates palette colors selected from the neighbor cache from colors that require literal coding.
/// </summary>
/// <param name="colorCache">The sorted unique neighbor colors.</param>
/// <param name="colors">The sorted palette colors.</param>
/// <param name="cacheColorFound">The cache-selection flags.</param>
/// <param name="uncachedColors">The destination for colors absent from the cache.</param>
/// <returns>The number of uncached colors.</returns>
private static int IndexColorCache(
ReadOnlySpan<ushort> colorCache,
ReadOnlySpan<ushort> colors,
Span<byte> cacheColorFound,
Span<ushort> uncachedColors)
{
cacheColorFound[..colorCache.Length].Clear();
Span<byte> inCache = stackalloc byte[Av1Constants.PaletteMaxSize];
inCache.Clear();
// Cache-order flags drive the bitstream while palette-order flags preserve the sorted uncached output.
int cachedColorCount = 0;
for (int cacheIndex = 0; cacheIndex < colorCache.Length && cachedColorCount < colors.Length; cacheIndex++)
{
for (int colorIndex = 0; colorIndex < colors.Length; colorIndex++)
{
if (colors[colorIndex] == colorCache[cacheIndex])
{
inCache[colorIndex] = 1;
cacheColorFound[cacheIndex] = 1;
cachedColorCount++;
break;
}
}
}
int uncachedColorCount = 0;
for (int colorIndex = 0; colorIndex < colors.Length; colorIndex++)
{
if (inCache[colorIndex] == 0)
{
uncachedColors[uncachedColorCount++] = colors[colorIndex];
}
}
return uncachedColorCount;
}
/// <summary>
/// Gets the literal length of an ascending palette-color sequence.
/// </summary>
/// <param name="colors">The sorted colors.</param>
/// <param name="bitDepth">The number of bits in each color sample.</param>
/// <param name="minimumDelta">The minimum representable difference between adjacent colors.</param>
/// <returns>The literal length in bits.</returns>
private static int GetDeltaEncodedColorBitCount(
ReadOnlySpan<ushort> colors,
int bitDepth,
int minimumDelta)
{
if (colors.IsEmpty)
{
return 0;
}
int bitCount = bitDepth;
if (colors.Length == 1)
{
return bitCount;
}
int maximumDelta = 0;
for (int i = 1; i < colors.Length; i++)
{
maximumDelta = Math.Max(maximumDelta, colors[i] - colors[i - 1]);
}
int minimumBits = bitDepth - 3;
int bits = Math.Max(
(int)Av1Math.CeilLog2((uint)(maximumDelta + 1 - minimumDelta)),
minimumBits);
int range = (1 << bitDepth) - colors[0] - minimumDelta;
bitCount += 2;
for (int i = 1; i < colors.Length; i++)
{
int delta = colors[i] - colors[i - 1];
bitCount += bits;
range -= delta;
bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range));
}
return bitCount;
}
/// <summary>
/// Writes an ascending palette-color sequence as one literal followed by bounded deltas.
/// </summary>
/// <param name="colors">The sorted colors.</param>
/// <param name="bitDepth">The number of bits in each color sample.</param>
/// <param name="minimumDelta">The minimum representable difference between adjacent colors.</param>
private void WriteDeltaEncodedColors(
ReadOnlySpan<ushort> colors,
int bitDepth,
int minimumDelta)
{
if (colors.IsEmpty)
{
return;
}
this.WriteLiteral(colors[0], bitDepth);
if (colors.Length == 1)
{
return;
}
int maximumDelta = 0;
for (int i = 1; i < colors.Length; i++)
{
maximumDelta = Math.Max(maximumDelta, colors[i] - colors[i - 1]);
}
int minimumBits = bitDepth - 3;
int bits = Math.Max(
(int)Av1Math.CeilLog2((uint)(maximumDelta + 1 - minimumDelta)),
minimumBits);
this.WriteLiteral((uint)(bits - minimumBits), 2);
int range = (1 << bitDepth) - colors[0] - minimumDelta;
for (int i = 1; i < colors.Length; i++)
{
int delta = colors[i] - colors[i - 1];
this.WriteLiteral((uint)(delta - minimumDelta), bits);
range -= delta;
bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range));
}
}
/// <summary>
/// Gets the bit width required by wrapped V-plane palette deltas.
/// </summary>
/// <param name="colors">The V-plane colors in U-palette order.</param>
/// <param name="bitDepth">The number of bits in each color sample.</param>
/// <param name="zeroCount">The number of deltas that omit a sign bit.</param>
/// <param name="minimumBits">The minimum permitted delta width.</param>
/// <returns>The delta width in bits.</returns>
private static int GetPaletteVDeltaBitCount(
ReadOnlySpan<ushort> colors,
int bitDepth,
out int zeroCount,
out int minimumBits)
{
int sampleRange = 1 << bitDepth;
int maximumDelta = 0;
zeroCount = 0;
minimumBits = bitDepth - 4;
for (int i = 1; i < colors.Length; i++)
{
int delta = Math.Abs(colors[i] - colors[i - 1]);
int wrappedDelta = Math.Min(delta, sampleRange - delta);
maximumDelta = Math.Max(maximumDelta, wrappedDelta);
if (wrappedDelta == 0)
{
zeroCount++;
}
}
return Math.Max((int)Av1Math.CeilLog2((uint)(maximumDelta + 1)), minimumBits);
}
/// <summary> /// <summary>
/// Emits coefficient syntax and reports no estimated rate. /// Emits coefficient syntax and reports no estimated rate.
/// </summary> /// </summary>

135
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

@ -2659,40 +2659,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
scoped Span<ushort> colors) scoped Span<ushort> colors)
{ {
Span<ushort> colorCache = stackalloc ushort[Av1Constants.PaletteMaxSize * 2]; Span<ushort> colorCache = stackalloc ushort[Av1Constants.PaletteMaxSize * 2];
Span<ushort> cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
int cacheSize = GetPaletteCache(ref partitionInfo, Av1Plane.Y, colorCache); int cacheSize = GetPaletteCache(ref partitionInfo, Av1Plane.Y, colorCache);
int colorIndex = 0; reader.ReadPaletteYColors(colorCache[..cacheSize], paletteSize, bitDepth, colors);
for (int i = 0; i < cacheSize && colorIndex < paletteSize; i++)
{
if (reader.ReadLiteral(1) != 0)
{
cachedColors[colorIndex++] = colorCache[i];
}
}
if (colorIndex == paletteSize)
{
cachedColors[..paletteSize].CopyTo(colors);
return;
}
int cachedColorCount = colorIndex;
colors[colorIndex++] = (ushort)reader.ReadLiteral(bitDepth);
if (colorIndex < paletteSize)
{
int bits = bitDepth - 3 + reader.ReadLiteral(2);
int maximumColor = (1 << bitDepth) - 1;
int range = maximumColor - colors[colorIndex - 1];
for (; colorIndex < paletteSize; colorIndex++)
{
int delta = reader.ReadLiteral(bits) + 1;
colors[colorIndex] = (ushort)Av1Math.Clip3(0, maximumColor, colors[colorIndex - 1] + delta);
range -= colors[colorIndex] - colors[colorIndex - 1];
bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range));
}
}
MergePaletteColors(colors, cachedColors, paletteSize, cachedColorCount);
} }
/// <summary> /// <summary>
@ -2713,77 +2681,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
scoped Span<ushort> vColors) scoped Span<ushort> vColors)
{ {
Span<ushort> colorCache = stackalloc ushort[Av1Constants.PaletteMaxSize * 2]; Span<ushort> colorCache = stackalloc ushort[Av1Constants.PaletteMaxSize * 2];
Span<ushort> cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize];
int cacheSize = GetPaletteCache(ref partitionInfo, Av1Plane.U, colorCache); int cacheSize = GetPaletteCache(ref partitionInfo, Av1Plane.U, colorCache);
int colorIndex = 0; reader.ReadPaletteUvColors(colorCache[..cacheSize], paletteSize, bitDepth, uColors, vColors);
for (int i = 0; i < cacheSize && colorIndex < paletteSize; i++)
{
if (reader.ReadLiteral(1) != 0)
{
cachedColors[colorIndex++] = colorCache[i];
}
}
if (colorIndex < paletteSize)
{
int cachedColorCount = colorIndex;
uColors[colorIndex++] = (ushort)reader.ReadLiteral(bitDepth);
if (colorIndex < paletteSize)
{
int bits = bitDepth - 3 + reader.ReadLiteral(2);
int maximumColor = (1 << bitDepth) - 1;
int range = (1 << bitDepth) - uColors[colorIndex - 1];
for (; colorIndex < paletteSize; colorIndex++)
{
int delta = reader.ReadLiteral(bits);
uColors[colorIndex] = (ushort)Av1Math.Clip3(0, maximumColor, uColors[colorIndex - 1] + delta);
range -= uColors[colorIndex] - uColors[colorIndex - 1];
bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range));
}
}
MergePaletteColors(uColors, cachedColors, paletteSize, cachedColorCount);
}
else
{
cachedColors[..paletteSize].CopyTo(uColors);
}
if (reader.ReadLiteral(1) != 0)
{
// V deltas wrap in the unsigned sample domain so complementary chroma colors remain compact.
int bits = bitDepth - 4 + reader.ReadLiteral(2);
int maximumColorPlusOne = 1 << bitDepth;
vColors[0] = (ushort)reader.ReadLiteral(bitDepth);
for (int i = 1; i < paletteSize; i++)
{
int delta = reader.ReadLiteral(bits);
if (delta != 0 && reader.ReadLiteral(1) != 0)
{
delta = -delta;
}
int value = vColors[i - 1] + delta;
if (value < 0)
{
value += maximumColorPlusOne;
}
if (value >= maximumColorPlusOne)
{
value -= maximumColorPlusOne;
}
vColors[i] = (ushort)value;
}
}
else
{
for (int i = 0; i < paletteSize; i++)
{
vColors[i] = (ushort)reader.ReadLiteral(bitDepth);
}
}
} }
/// <summary> /// <summary>
@ -2858,36 +2757,6 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
} }
} }
/// <summary>
/// Merges selected cached colors with the sorted transmitted colors in one prediction-order palette.
/// </summary>
/// <param name="colors">The transmitted colors beginning at <paramref name="cachedColorCount"/> and the merged output.</param>
/// <param name="cachedColors">The selected cached colors in ascending order.</param>
/// <param name="paletteSize">The total palette size.</param>
/// <param name="cachedColorCount">The number of selected cached colors.</param>
private static void MergePaletteColors(Span<ushort> colors, ReadOnlySpan<ushort> cachedColors, int paletteSize, int cachedColorCount)
{
if (cachedColorCount == 0)
{
return;
}
int cacheIndex = 0;
int transmittedIndex = cachedColorCount;
for (int i = 0; i < paletteSize; i++)
{
if (cacheIndex < cachedColorCount &&
(transmittedIndex >= paletteSize || cachedColors[cacheIndex] <= colors[transmittedIndex]))
{
colors[i] = cachedColors[cacheIndex++];
}
else
{
colors[i] = colors[transmittedIndex++];
}
}
}
/// <summary> /// <summary>
/// Computes the padded plane dimensions and the portion that lies inside the coded image. /// Computes the padded plane dimensions and the portion that lies inside the coded image.
/// </summary> /// </summary>

88
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

@ -1145,6 +1145,94 @@ public class Av1EntropyTests
Assert.Equal(Av1ProbabilityCost.GetLiteralCost(3), Av1SymbolEncoder.GetUniformCost(5, 3)); Assert.Equal(Av1ProbabilityCost.GetLiteralCost(3), Av1SymbolEncoder.GetUniformCost(5, 3));
} }
[Theory]
[InlineData(8)]
[InlineData(10)]
[InlineData(12)]
public void RoundTripPaletteColors(int bitDepth)
{
ushort[] colorCache = [4, 17, 23, 51];
ushort[] yColors = [4, 23, 90];
ushort[] uColors = [17, 51, 100];
ushort[] deltaVColors = [1, 2, 1];
ushort[] rawVColors = [0, (ushort)(1 << (bitDepth - 1)), 0];
using Av1SymbolEncoder encoder = new(Configuration.Default, 128, BaseQIndex);
encoder.WritePaletteYColors(colorCache, yColors, bitDepth);
encoder.WritePaletteUvColors(colorCache, uColors, deltaVColors, bitDepth);
encoder.WritePaletteUvColors(colorCache, uColors, rawVColors, bitDepth);
using IMemoryOwner<byte> encoded = encoder.Exit();
Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex);
ushort[] decodedYColors = new ushort[yColors.Length];
ushort[] decodedUColors = new ushort[uColors.Length];
ushort[] decodedDeltaVColors = new ushort[deltaVColors.Length];
ushort[] decodedRawVColors = new ushort[rawVColors.Length];
decoder.ReadPaletteYColors(colorCache, yColors.Length, bitDepth, decodedYColors);
decoder.ReadPaletteUvColors(
colorCache,
uColors.Length,
bitDepth,
decodedUColors,
decodedDeltaVColors);
Assert.Equal(yColors, decodedYColors);
Assert.Equal(uColors, decodedUColors);
Assert.Equal(deltaVColors, decodedDeltaVColors);
decodedUColors.AsSpan().Clear();
decoder.ReadPaletteUvColors(
colorCache,
uColors.Length,
bitDepth,
decodedUColors,
decodedRawVColors);
Assert.Equal(uColors, decodedUColors);
Assert.Equal(rawVColors, decodedRawVColors);
}
[Fact]
public void PaletteColorCostsMatchCurrentLibaomBitCounts()
{
ushort[] colorCache = [5, 10, 20];
Assert.Equal(
Av1ProbabilityCost.GetLiteralCost(20),
Av1SymbolEncoder.GetPaletteYColorCost([], [10, 20, 21], 8));
Assert.Equal(
Av1ProbabilityCost.GetLiteralCost(11),
Av1SymbolEncoder.GetPaletteYColorCost(colorCache, [5, 20, 30], 8));
Assert.Equal(
Av1ProbabilityCost.GetLiteralCost(32),
Av1SymbolEncoder.GetPaletteUvColorCost(colorCache, [5, 20, 30], [20, 21, 20], 8));
Assert.Equal(
Av1ProbabilityCost.GetLiteralCost(36),
Av1SymbolEncoder.GetPaletteUvColorCost(colorCache, [5, 20, 30], [0, 128, 0], 8));
}
[Fact]
public void PaletteColorCostDoesNotAllocate()
{
ushort[] colorCache = [5, 10, 20];
ushort[] yColors = [5, 20, 30];
ushort[] uColors = [5, 20, 30];
ushort[] vColors = [20, 21, 20];
_ = Av1SymbolEncoder.GetPaletteYColorCost(colorCache, yColors, 8);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1000; i++)
{
_ = Av1SymbolEncoder.GetPaletteYColorCost(colorCache, yColors, 8);
_ = Av1SymbolEncoder.GetPaletteUvColorCost(colorCache, uColors, vColors, 8);
}
long after = GC.GetAllocatedBytesForCurrentThread();
Assert.Equal(before, after);
}
[Theory] [Theory]
[MemberData(nameof(GetRangeData), 20)] [MemberData(nameof(GetRangeData), 20)]
public void RoundTripPartitionType(int context) public void RoundTripPartitionType(int context)

Loading…
Cancel
Save