From 1098109df3bcb8439f719bac25f2ad00290250a6 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 26 Aug 2026 12:02:13 +1000 Subject: [PATCH] Unify HEIF planar color conversion --- HEIF_IMPLEMENTATION_PLAN.md | 5 +- src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs | 1 + .../Heif/Av1/Av1YuvConverter.RowConverters.cs | 455 ----------- .../Formats/Heif/Av1/Av1YuvConverter.cs | 174 ----- .../Heif/Av1/Color/Av1PlanarSampleBuffer.cs | 133 ++++ .../Formats/Heif/Av1/Color/Av1YuvConverter.cs | 109 +++ .../Color/HeifColorConversionParameters.cs | 18 + .../Heif/Color/HeifPlanarColorConverter.cs | 738 ++++++++++++++++++ .../Heif/Color/HeifYuv420ToRgb8Converter.cs | 18 +- .../Heif/Color/IHeifPlanarSampleBuffer.cs | 78 ++ .../Heif/Color/IHeifYuv420ToRgb8Source.cs | 31 - .../Heif/Hevc/Color/HevcPlanarSampleBuffer.cs | 81 ++ .../Heif/Hevc/Color/HevcYuv420ToRgb8Source.cs | 32 - .../Heif/Hevc/Color/HevcYuvConverter.cs | 597 +------------- .../Heif/Av1ColorConversionBenchmarks.cs | 1 + .../Formats/Heif/Av1/Av1YuvConverterTests.cs | 35 + 16 files changed, 1224 insertions(+), 1282 deletions(-) delete mode 100644 src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs delete mode 100644 src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs create mode 100644 src/ImageSharp/Formats/Heif/Color/HeifPlanarColorConverter.cs create mode 100644 src/ImageSharp/Formats/Heif/Color/IHeifPlanarSampleBuffer.cs delete mode 100644 src/ImageSharp/Formats/Heif/Color/IHeifYuv420ToRgb8Source.cs create mode 100644 src/ImageSharp/Formats/Heif/Hevc/Color/HevcPlanarSampleBuffer.cs delete mode 100644 src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Source.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 50748dea9f..a486b0134c 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -41,7 +41,7 @@ Checkboxes may be marked complete only when the implementation and the verificat - [x] Verify IPT-C2 against independently calculated matrix values and verify YCgCo-Re/YCgCo-Ro against exact 8/10/12-bit code values, limited-range black/white values, and SIMD-lane/scalar-tail parity. - [x] Implement SIMD 4:2:0 and 4:2:2 encoder downsampling with odd-width and odd-height tails, and verify the stored 8/12-bit chroma samples against independently encoded full-resolution planes. - [x] Move the H.273 parameter resolver, closed color operators, transfer functions, sample loading/storage, chroma filtering, and RGB packing into one shared HEIF color pipeline used by both AV1 and HEVC rather than maintaining codec-specific arithmetic copies. - - [x] Keep optimized color arithmetic, fixed-point parameter storage, SIMD-width traversal, sample loading/storage, chroma filtering, and RGB packing primitives in the shared HEIF color namespace. AV1 and HEVC namespaces adapt their native planes, crop windows, and signaled chroma positions without owning parallel color arithmetic. + - [x] Drive AV1 and HEVC through the same `IHeifPlanarSampleBuffer` and `HeifPlanarColorConverter` architecture. Codec namespaces now adapt only native rows, sample precision, subsampling, crop windows, chroma positions, and codec color signaling; pooled row scheduling, chroma reconstruction/downsampling, fixed-point specialization, SIMD traversal, scalar tails, and RGB packing are selected by the shared HEIF driver. - [x] Implement sequential pooled HEVC conversion in both directions for monochrome, 4:2:0, 4:2:2, and 4:4:4; independent luma/chroma precision; 8/10/12-bit full/limited ranges; and all six progressive 4:2:0 chroma sample locations. Eight-bit paths use JPEG's optimized RGB plane contracts, while high-bit-depth paths retain 16-bit packed pixels. - [x] Implement allocation-free SIMD-first HEVC intra prediction for all 35 modes, 4/8/16/32 blocks, and 8/10/12-bit samples, including three-tap and strong-bilinear reference filtering, negative-angle reference extension, luma boundary filters, and SIMD horizontal transposition. Verify the normal and forced-scalar paths against one scalar oracle. - [x] Match HM's channel-specific smoothing rules: combined 4:4:4 chroma uses the normal three-tap filter when selected, while strong bilinear smoothing remains restricted to luma syntax. Exact official RExt reconstruction passes at 8, 10, and 12 bits. @@ -63,6 +63,7 @@ Checkboxes may be marked complete only when the implementation and the verificat - [x] Add permanent frame-wide `Av1ColorConversionBenchmarks` coverage for sequential 1920x1080 YUV 4:2:0 conversion in both directions at 8, 10, and 12 bits, including managed-allocation reporting. - [x] Record the .NET 10 short-run AV1 baseline on 2026-08-25. SIMD decode measured 4.025/2.803/2.742 ms and encode measured 3.750/2.555/2.450 ms at 8/10/12 bits with 488-744 B allocated per frame. Forced-scalar decode measured 31.15/29.47/28.40 ms and encode measured 19.19/15.77/15.14 ms, making the normal SIMD pipeline 7.7-10.5 times faster for decode and 5.1-6.2 times faster for encode on this machine. - [x] Add permanent frame-wide `HevcColorConversionBenchmarks` coverage and record the .NET 10 short-run plane-conversion baseline on 2026-08-26. SIMD decode measured 1.141/2.640/2.668 ms and encode measured 2.089/2.481/2.412 ms at 8/10/12 bits with 504-712 B allocated per frame. Forced-scalar decode measured 13.42/28.35/27.60 ms and encode measured 14.82/14.67/14.45 ms, making the normal SIMD pipeline 10.3-11.8 times faster for decode and 5.9-7.1 times faster for encode on this machine. + - [x] Re-verify the unified AV1/HEVC driver with all 127 focused color and exact HEIC presentation cases under normal SIMD dispatch and with all hardware intrinsics disabled. Re-run all 12 permanent 1920x1080 plane-conversion benchmarks; BenchmarkDotNet could not hold the configured power plan and the CPU changed performance state during warmup, so do not replace the controlled baselines above with those throttled sustained means. - [ ] Add representative ICC-enabled AVIF cases and the equivalent HEVC cases after the HEVC reconstruction path is complete. - [x] **Completed:** remove production `ToArray()` materializations from the HEIF implementation and make every retained copy an explicit ownership decision. - [x] Store AV1 palette colors and HEVC short-term reference-picture state inline at their normative fixed bounds. @@ -216,7 +217,7 @@ This snapshot pins or classifies the available references and failures; it does | Managed implementation | Normative behavior | Reviewed implementation reference | Use | | --- | --- | --- | --- | -| `HeifColorConversionParameters`, `HeifColorConverter`, `Av1YuvConverter`, and `HevcYuvConverter` | ITU-T H.273 (V4) equations 14-16 and 27-84, including limited/full-range scaling, chromaticity-derived equations 39-47, YCgCo equations 51-57, constant-luminance equations 66-75, IPT-C2, YCgCo-Re/YCgCo-Ro, and the PQ/HLG ICtCp matrices; AV1 section 6.4.2 and HEVC Annex E chroma sample positions | Official ITU-T H.273 (V4) (07/2024); libavif `src/reformat.c` and `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826`; libaom `aom/aom_image.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar behavioral oracle for full/limited-range conversion at 8, 10, and 12 bits. Decode and encode cover every H.273 V4 matrix code point through 17: identity, YCgCo, coefficient-based NCL, both fixed and chromaticity-derived constant/non-constant-luminance systems, SMPTE ST 2085, ICtCp, IPT-C2, YCgCo-Re, and YCgCo-Ro. Shared paths cover monochrome, YUV 4:2:0, 4:2:2, and 4:4:4; AV1 centered/co-sited placement; and all six HEVC progressive 4:2:0 sample locations. Identity and reversible lifting enforce their full-chroma constraint. Limited-range YCgCo retains the 219-code scale inherited from its R/G/B inputs instead of applying YCbCr's unrelated 224-code chroma range. YCgCo-Re and YCgCo-Ro apply range adjustment at reduced-precision RGB code values before signed integer lifting. Chromaticity derivation uses every defined H.273 primary and matches libavif's BT.709 fallback for unspecified or reserved primaries. ICtCp and IPT-C2 inverses come from the exact H.273 matrices rather than unrelated display conversions. Closed operators provide `Vector512`, `Vector256`, and `Vector128` implementations with one scalar tail. Eight-bit codec wrappers use JPEG's RGB plane pack/unpack contracts; high-bit-depth wrappers retain native 16-bit packed pixels. | +| `HeifColorConversionParameters`, `HeifColorConverter`, `IHeifPlanarSampleBuffer`, `HeifPlanarColorConverter`, `Av1YuvConverter`, and `HevcYuvConverter` | ITU-T H.273 (V4) equations 14-16 and 27-84, including limited/full-range scaling, chromaticity-derived equations 39-47, YCgCo equations 51-57, constant-luminance equations 66-75, IPT-C2, YCgCo-Re/YCgCo-Ro, and the PQ/HLG ICtCp matrices; AV1 section 6.4.2 and HEVC Annex E chroma sample positions | Official ITU-T H.273 (V4) (07/2024); libavif `src/reformat.c` and `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826`; libaom `aom/aom_image.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar behavioral oracle for full/limited-range conversion at 8, 10, and 12 bits. Decode and encode cover every H.273 V4 matrix code point through 17: identity, YCgCo, coefficient-based NCL, both fixed and chromaticity-derived constant/non-constant-luminance systems, SMPTE ST 2085, ICtCp, IPT-C2, YCgCo-Re, and YCgCo-Ro. One closed generic planar driver owns pooled sequential row traversal, chroma reconstruction/downsampling, fixed-point specialization, SIMD dispatch, scalar tails, and packed-pixel conversion for both AV1 and HEVC adapters. Shared paths cover monochrome, YUV 4:2:0, 4:2:2, and 4:4:4; AV1 centered/co-sited placement; and all six HEVC progressive 4:2:0 sample locations. Identity and reversible lifting enforce their full-chroma constraint. Limited-range YCgCo retains the 219-code scale inherited from its R/G/B inputs instead of applying YCbCr's unrelated 224-code chroma range. YCgCo-Re and YCgCo-Ro apply range adjustment at reduced-precision RGB code values before signed integer lifting. Chromaticity derivation uses every defined H.273 primary and matches libavif's BT.709 fallback for unspecified or reserved primaries. ICtCp and IPT-C2 inverses come from the exact H.273 matrices rather than unrelated display conversions. Closed operators provide `Vector512`, `Vector256`, and `Vector128` implementations with one scalar tail. Eight-bit conversion uses JPEG's RGB plane pack/unpack contracts; high-bit-depth conversion retains native 16-bit packed pixels. | | `HeifTransferFunctions` | ITU-T H.273 (V4) Table 3 transfer characteristics 1-18 | Official ITU-T H.273 (V4) (07/2024); libavif `src/colr.c` at `092276ce89098ead06db80975173191e5fee1826` | Apply every HEVC/AV1-signallable transfer function required by constant-luminance, ICtCp, and IPT-C2 conversion. Retain the H.273 normalized PQ and HLG definitions; do not import libavif's display-oriented 203-nit scaling or HLG OOTF into codec sample interpretation. Use libavif's midpoint convention only for the non-bijective zero code of the two logarithmic curves. | | `ObuReader.ReadSequenceHeader`, `ReadUncompressedFrameHeader`, decoder-model parsing, and operating-parameter consumption | AV1 sections 5.5.2 through 5.5.4 sequence timing and decoder-model syntax, section 5.9.2 uncompressed frame-header syntax, and section 5.9.31 temporal-point syntax | libaom `av1/decoder/decodeframe.c` functions `av1_read_decoder_model_info`, `av1_read_op_parameters_info`, `read_temporal_point_info`, and `read_uncompressed_header`, plus `common/av1_config.c`, at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Read the normative 32-bit decoding-tick field; consume operating-point buffer delays and the layer-applicable frame removal times needed to keep a non-reduced still-image sequence aligned; and read presentation time only under the normative decoder-model condition. Retain no scheduling or playback behavior from those values and introduce no ISO BMFF timing, track, sample-table, or sequence surface. | | `Av1FrameBuffer` high-bit-depth sample layout and `Av1YuvConverter` 10/12-bit packed-pixel conversion | AV1 section 6.4.1 bit depth and H.273 sample-range scaling | libaom `aom_scale/yv12config.h`, `av1/common/idct.c`, and `av1/common/reconintra.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f`; libavif `src/avif.c` and `src/reformat.c` at `092276ce89098ead06db80975173191e5fee1826` | Establish two-byte native sample storage with sample-unit strides for 10/12-bit reconstruction and use ImageSharp's existing `Rgb48` pixel-operation paths in both directions so packed-pixel staging does not reduce high-bit-depth samples to eight bits. | diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs index 2baeaf373e..61e36fb21d 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Heif.Av1.Color; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs deleted file mode 100644 index 25ba6f8332..0000000000 --- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.RowConverters.cs +++ /dev/null @@ -1,455 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; -using SixLabors.ImageSharp.Formats.Heif.Color; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using static SixLabors.ImageSharp.Formats.Heif.Color.HeifColorConverterBase; - -namespace SixLabors.ImageSharp.Formats.Heif.Av1; - -/// -/// Provides the pooled row converters used by AV1 color conversion. -/// -internal static partial class Av1YuvConverter -{ - /// - /// Converts one reconstructed AV1 row using frame-scoped pooled component, chroma, and packed-pixel storage. - /// - /// The destination pixel type. - /// The reconstructed sample type. - /// The SIMD widening operations for the sample type. - private readonly struct YuvToRgbRowConverter - where TPixel : unmanaged, IPixel - where TSample : unmanaged - where TLoader : struct, IHeifSampleLoader - { - /// - /// The configuration used by bulk pixel conversion. - /// - private readonly Configuration configuration; - - /// - /// The reconstructed AV1 frame containing the source planes. - /// - private readonly Av1FrameBuffer frameBuffer; - - /// - /// The destination image frame. - /// - private readonly ImageFrame image; - - /// - /// The full-resolution luma plane. - /// - private readonly Buffer2DRegion yPlane; - - /// - /// The blue-difference plane when the frame contains chroma. - /// - private readonly Buffer2DRegion uPlane; - - /// - /// The red-difference plane when the frame contains chroma. - /// - private readonly Buffer2DRegion vPlane; - - /// - /// The frame-scoped color-model converter. - /// - private readonly HeifColorConverterBase colorConverter; - - /// - /// The signaled chroma sample position used for reconstruction. - /// - private readonly ObuChromoSamplePosition chromaSamplePosition; - - /// - /// Whether the source contains only a luma plane. - /// - private readonly bool isMonochrome; - - /// - /// The horizontal chroma subsampling shift. - /// - private readonly int subX; - - /// - /// The vertical chroma subsampling shift. - /// - private readonly int subY; - - /// - /// Initializes a new instance of the struct. - /// - /// The configuration used for packed-pixel conversion. - /// The reconstructed AV1 frame. - /// The destination image frame. - /// The selected H.273 color converter. - public YuvToRgbRowConverter( - Configuration configuration, - Av1FrameBuffer frameBuffer, - ImageFrame image, - HeifColorConverterBase colorConverter) - { - this.configuration = configuration; - this.frameBuffer = frameBuffer; - this.image = image; - this.colorConverter = colorConverter; - this.isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; - this.subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0; - this.subY = frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; - this.chromaSamplePosition = frameBuffer.ColorConfig.ChromaSamplePosition; - this.yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); - this.uPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, this.subX, this.subY); - this.vPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, this.subX, this.subY); - } - - /// - /// Gets the number of float elements required by the reusable conversion buffer. - /// - public int BufferLength - { - get - { - // Three float component rows are converted in place. Color input adds two reusable chroma scratch - // rows, and the final one or two float-sized slots per pixel back RGB byte planes or an Rgba64 row. - int rowCount = this.isMonochrome ? 3 : 5; - int packedRowCount = typeof(TSample) == typeof(byte) ? 1 : 2; - return this.image.Width * (rowCount + packedRowCount); - } - } - - /// - /// Converts one reconstructed AV1 row to packed pixels. - /// - /// The row index. - /// The reusable conversion buffer. - public void Convert(int y, Span span) - { - int width = this.image.Width; - Span red = span[..width]; - Span green = span.Slice(width, width); - Span blue = span.Slice(width * 2, width); - - ReadOnlySpan ySource; - if (typeof(TSample) == typeof(byte)) - { - ySource = MemoryMarshal.Cast(this.yPlane.DangerousGetRowSpan(y)); - } - else - { - ySource = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0)); - } - - ConvertSamplesToFloat(ySource, red); - - int packedOffset = width * 3; - if (!this.isMonochrome) - { - GetChromaCoordinates( - y, - this.subY, - this.subY != 0 && this.chromaSamplePosition != ObuChromoSamplePosition.Colocated, - this.uPlane.Height - 1, - out int y0, - out int y1, - out int y1Weight); - - ReadOnlySpan uRow0; - ReadOnlySpan uRow1; - ReadOnlySpan vRow0; - ReadOnlySpan vRow1; - if (typeof(TSample) == typeof(byte)) - { - uRow0 = MemoryMarshal.Cast(this.uPlane.DangerousGetRowSpan(y0)); - uRow1 = MemoryMarshal.Cast(this.uPlane.DangerousGetRowSpan(y1)); - vRow0 = MemoryMarshal.Cast(this.vPlane.DangerousGetRowSpan(y0)); - vRow1 = MemoryMarshal.Cast(this.vPlane.DangerousGetRowSpan(y1)); - } - else - { - uRow0 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y0, this.subX, this.subY)); - uRow1 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, y1, this.subX, this.subY)); - vRow0 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y0, this.subX, this.subY)); - vRow1 = MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, y1, this.subX, this.subY)); - } - - Span scratch0 = span.Slice(width * 3, width); - Span scratch1 = span.Slice(width * 4, width); - bool isCenteredX = this.subX != 0 && (this.subY == 0 || this.chromaSamplePosition == ObuChromoSamplePosition.Unknown); - ReconstructChromaRow(uRow0, uRow1, y1Weight, this.subX, isCenteredX, green, scratch0, scratch1); - ReconstructChromaRow(vRow0, vRow1, y1Weight, this.subX, isCenteredX, blue, scratch0, scratch1); - packedOffset = width * 5; - } - - this.colorConverter.ConvertToRgbInPlace(red, green, blue); - Span packedStorage = span[packedOffset..]; - Span destination = this.image.PixelBuffer.DangerousGetRowSpan(y); - if (typeof(TSample) == typeof(byte)) - { - // Match JPEG's byte-plane packing contract so optimized pixel types use the existing RGB packer. - // The final float row provides enough byte storage for three tightly packed component planes. - Span byteStorage = MemoryMarshal.AsBytes(packedStorage)[..(width * 3)]; - Span redBytes = byteStorage[..width]; - Span greenBytes = byteStorage.Slice(width, width); - Span blueBytes = byteStorage.Slice(width * 2, width); - SimdUtils.NormalizedFloatToByteSaturate(red, redBytes); - SimdUtils.NormalizedFloatToByteSaturate(green, greenBytes); - SimdUtils.NormalizedFloatToByteSaturate(blue, blueBytes); - - PixelOperations.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, destination); - } - else - { - Span packed = MemoryMarshal.Cast(packedStorage)[..width]; - PackRgba64(red, green, blue, packed); - PixelOperations.Instance.FromRgba64(this.configuration, packed, destination); - } - } - } - - /// - /// Converts one source row, or one vertically subsampled row pair, using reusable pooled component storage. - /// - /// The source pixel type. - /// The encoded sample type. - /// The SIMD narrowing and storage operations for the sample type. - private readonly struct RgbToYuvRowConverter - where TPixel : unmanaged, IPixel - where TSample : unmanaged - where TStorer : struct, IHeifSampleStorer - { - /// - /// The configuration used by bulk pixel conversion. - /// - private readonly Configuration configuration; - - /// - /// The destination AV1 frame. - /// - private readonly Av1FrameBuffer frameBuffer; - - /// - /// The source image frame. - /// - private readonly ImageFrame image; - - /// - /// The full-resolution luma plane. - /// - private readonly Buffer2DRegion yPlane; - - /// - /// The blue-difference plane when the frame contains chroma. - /// - private readonly Buffer2DRegion uPlane; - - /// - /// The red-difference plane when the frame contains chroma. - /// - private readonly Buffer2DRegion vPlane; - - /// - /// The frame-scoped color-model converter. - /// - private readonly HeifColorConverterBase colorConverter; - - /// - /// The largest value represented by the encoded AV1 bit depth. - /// - private readonly float sampleMaximum; - - /// - /// Whether the destination contains only a luma plane. - /// - private readonly bool isMonochrome; - - /// - /// The horizontal chroma subsampling shift. - /// - private readonly int subX; - - /// - /// The vertical chroma subsampling shift. - /// - private readonly int rowShift; - - /// - /// Initializes a new instance of the struct. - /// - /// The configuration used for packed-pixel conversion. - /// The destination AV1 frame. - /// The source image frame. - /// The selected H.273 color converter. - /// The largest encoded sample value. - public RgbToYuvRowConverter( - Configuration configuration, - Av1FrameBuffer frameBuffer, - ImageFrame image, - HeifColorConverterBase colorConverter, - float sampleMaximum) - { - this.configuration = configuration; - this.frameBuffer = frameBuffer; - this.image = image; - this.colorConverter = colorConverter; - this.sampleMaximum = sampleMaximum; - this.isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; - this.subX = frameBuffer.ColorConfig.SubSamplingX ? 1 : 0; - this.rowShift = !this.isMonochrome && frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; - this.yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); - this.uPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.U, this.subX, this.rowShift); - this.vPlane = this.isMonochrome ? default : frameBuffer.DeriveBlockPointer(Av1Plane.V, this.subX, this.rowShift); - } - - /// - /// Gets the number of float elements required by the reusable conversion buffer. - /// - public int ComponentBufferLength - { - get - { - // Three planar rows hold the converted YUV values, and 4:2:0 keeps a second set until its - // chroma has been averaged with the first. - int componentRowCount = this.rowShift == 0 ? 3 : 6; - return this.image.Width * componentRowCount; - } - } - - /// - /// Converts one source row, or one vertically subsampled row pair, to AV1 planes. - /// - /// The conversion iteration index. - /// The reusable high-bit-depth RGB staging row. - /// The reusable planar component buffer. - public void Convert(int y, Span packed, Span components) - { - int width = this.image.Width; - int sourceY = y << this.rowShift; - Span yRow0 = components[..width]; - Span cbRow0 = components.Slice(width, width); - Span crRow0 = components.Slice(width * 2, width); - this.ConvertSourceRow(sourceY, packed, yRow0, cbRow0, crRow0); - WriteSamples( - yRow0, - this.GetPlaneRow(Av1Plane.Y, sourceY), - this.colorConverter.LumaScale, - this.colorConverter.LumaBias, - this.sampleMaximum); - - bool hasSecondSourceRow = this.rowShift != 0 && sourceY + 1 < this.image.Height; - Span yRow1 = Span.Empty; - Span cbRow1 = Span.Empty; - Span crRow1 = Span.Empty; - if (hasSecondSourceRow) - { - yRow1 = components.Slice(width * 3, width); - cbRow1 = components.Slice(width * 4, width); - crRow1 = components.Slice(width * 5, width); - this.ConvertSourceRow(sourceY + 1, packed, yRow1, cbRow1, crRow1); - WriteSamples( - yRow1, - this.GetPlaneRow(Av1Plane.Y, sourceY + 1), - this.colorConverter.LumaScale, - this.colorConverter.LumaBias, - this.sampleMaximum); - } - - if (this.isMonochrome) - { - return; - } - - Span uDestination = this.GetPlaneRow(Av1Plane.U, y); - Span vDestination = this.GetPlaneRow(Av1Plane.V, y); - float chromaScale = this.colorConverter.ChromaScale; - float chromaBias = this.colorConverter.ChromaBias; - if (this.subX == 0) - { - WriteSamples(cbRow0, uDestination, chromaScale, chromaBias, this.sampleMaximum); - WriteSamples(crRow0, vDestination, chromaScale, chromaBias, this.sampleMaximum); - } - else - { - float secondRowWeight = hasSecondSourceRow ? 0.5F : 0F; - WriteSubsampledSamples( - cbRow0, - cbRow1, - uDestination, - true, - secondRowWeight, - chromaScale, - chromaBias, - this.sampleMaximum); - - WriteSubsampledSamples( - crRow0, - crRow1, - vDestination, - true, - secondRowWeight, - chromaScale, - chromaBias, - this.sampleMaximum); - } - } - - /// - /// Converts one source row into normalized planar AV1 components. - /// - /// The source row index. - /// The high-bit-depth RGB staging row. - /// The destination luma values. - /// The destination blue-difference values. - /// The destination red-difference values. - private void ConvertSourceRow(int y, Span packed, Span luma, Span chromaBlue, Span chromaRed) - { - ReadOnlySpan source = this.image.PixelBuffer.DangerousGetRowSpan(y); - if (typeof(TSample) == typeof(byte)) - { - // This is the same planar input contract used by JPEG encoding. Pixel types with optimized - // unpackers reach their existing SIMD path before the AV1 operator consumes the planes. - PixelOperations.Instance.UnpackIntoRgbPlanes(luma, chromaBlue, chromaRed, source); - this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, ByteMaximum); - } - else - { - // JPEG's planar unpack contract is eight-bit. AV1 10/12-bit encoding stages Rgb48 instead - // so high-precision source pixels are not truncated before the color transform. - PixelOperations.Instance.ToRgb48(this.configuration, source, packed); - DeinterleaveRgb48(packed, luma, chromaBlue, chromaRed); - this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, UShortMaximum); - } - } - - /// - /// Gets a writable row from an eight-bit or high-bit-depth AV1 plane. - /// - /// The requested plane. - /// The row index in the requested plane. - /// The writable sample row. - private Span GetPlaneRow(Av1Plane plane, int y) - { - if (typeof(TSample) == typeof(byte)) - { - Buffer2DRegion region = plane switch - { - Av1Plane.Y => this.yPlane, - Av1Plane.U => this.uPlane, - _ => this.vPlane, - }; - - return MemoryMarshal.Cast(region.DangerousGetRowSpan(y)); - } - - int subX = plane == Av1Plane.Y ? 0 : this.subX; - int subY = plane == Av1Plane.Y ? 0 : this.rowShift; - return MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(plane, y, subX, subY)); - } - } -} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs deleted file mode 100644 index 7aff4f3153..0000000000 --- a/src/ImageSharp/Formats/Heif/Av1/Av1YuvConverter.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; -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.Av1; - -/// -/// Converts between reconstructed AV1 YUV planes and packed ImageSharp pixels. -/// -internal static partial class Av1YuvConverter -{ - /// - /// The largest value represented by an eight-bit packed RGB component. - /// - private const float ByteMaximum = byte.MaxValue; - - /// - /// The largest value represented by a 16-bit packed RGB component. - /// - private const float UShortMaximum = ushort.MaxValue; - - /// - /// Converts the reconstructed YUV planes to packed pixels. - /// - /// The destination pixel type. - /// The configuration used for allocation and pixel conversion. - /// The reconstructed AV1 frame. - /// The destination image frame. - public static void ConvertToRgb(Configuration configuration, Av1FrameBuffer frameBuffer, ImageFrame image) - where TPixel : unmanaged, IPixel - { - HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer, out HeifColorConversionMode mode); - - HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, frameBuffer.ColorFormat == Av1ColorFormat.Yuv400); - if (frameBuffer.BitDepth == Av1BitDepth.EightBit) - { - YuvToRgbRowConverter converter = new(configuration, frameBuffer, image, colorConverter); - using IMemoryOwner scratchOwner = configuration.MemoryAllocator.Allocate(converter.BufferLength); - Span scratch = scratchOwner.GetSpan(); - for (int y = 0; y < image.Height; y++) - { - converter.Convert(y, scratch); - } - } - else - { - YuvToRgbRowConverter converter = new(configuration, frameBuffer, image, colorConverter); - using IMemoryOwner owner = configuration.MemoryAllocator.Allocate(converter.BufferLength); - Span scratch = owner.GetSpan(); - for (int y = 0; y < image.Height; y++) - { - converter.Convert(y, scratch); - } - } - } - - /// - /// Converts packed pixels to the configured monochrome or YUV planes used by the AV1 encoder. - /// - /// The source pixel type. - /// The configuration used for allocation and pixel conversion. - /// The source image frame. - /// The destination AV1 frame. - public static void ConvertFromRgb(Configuration configuration, ImageFrame image, Av1FrameBuffer frameBuffer) - where TPixel : unmanaged, IPixel - { - HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer, out HeifColorConversionMode mode); - float sampleMaximum = parameters.LumaSampleMaximum; - - bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; - HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, isMonochrome); - int rowShift = !isMonochrome && frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; - int iterationCount = (image.Height + rowShift) >> rowShift; - if (frameBuffer.BitDepth == Av1BitDepth.EightBit) - { - RgbToYuvRowConverter converter = new(configuration, frameBuffer, image, colorConverter, sampleMaximum); - using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(converter.ComponentBufferLength); - Span components = componentOwner.GetSpan(); - for (int y = 0; y < iterationCount; y++) - { - converter.Convert(y, Span.Empty, components); - } - } - else - { - RgbToYuvRowConverter converter = new(configuration, frameBuffer, image, colorConverter, sampleMaximum); - using IMemoryOwner packedOwner = configuration.MemoryAllocator.Allocate(image.Width); - using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(converter.ComponentBufferLength); - Span packed = packedOwner.GetSpan()[..image.Width]; - Span components = componentOwner.GetSpan(); - for (int y = 0; y < iterationCount; y++) - { - converter.Convert(y, packed, components); - } - } - } - - /// - /// Resolves the H.273 conversion mode, matrix coefficients, and sample range for a frame. - /// - /// The AV1 frame containing the signaled color configuration. - /// The resolved conversion mode. - /// The resolved conversion parameters. - private static HeifColorConversionParameters GetConversionParameters(Av1FrameBuffer frameBuffer, out HeifColorConversionMode mode) - { - if (frameBuffer.ColorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Reserved) - { - throw new InvalidImageContentException("The reserved AV1 chroma sample position is invalid."); - } - - bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; - - return HeifColorConversionParameters.Create( - (CicpColorPrimaries)(byte)frameBuffer.ColorConfig.ColorPrimaries, - (CicpTransferCharacteristics)(byte)frameBuffer.ColorConfig.TransferCharacteristics, - (CicpMatrixCoefficients)(byte)frameBuffer.ColorConfig.MatrixCoefficients, - frameBuffer.ColorConfig.ColorRange, - frameBuffer.BitDepth.GetBitCount(), - frameBuffer.BitDepth.GetBitCount(), - isMonochrome, - frameBuffer.ColorFormat == Av1ColorFormat.Yuv444, - out mode); - } - - /// - /// Resolves the two chroma samples and quarter-sample weight surrounding a luma coordinate. - /// - /// The luma coordinate. - /// The chroma subsampling shift. - /// Whether chroma lies between neighboring luma samples. - /// The last available chroma coordinate. - /// The lower chroma coordinate. - /// The upper chroma coordinate. - /// The upper-coordinate weight with a denominator of four. - private static void GetChromaCoordinates( - int coordinate, - int subsampling, - bool isCentered, - int maximum, - out int lower, - out int upper, - out int upperWeight) - { - if (subsampling == 0) - { - lower = coordinate; - upper = coordinate; - upperWeight = 0; - return; - } - - int sample = coordinate >> 1; - bool isOdd = (coordinate & 1) != 0; - if (isCentered) - { - lower = isOdd ? sample : Math.Max(sample - 1, 0); - upper = isOdd ? Math.Min(sample + 1, maximum) : sample; - upperWeight = isOdd ? 1 : 3; - return; - } - - lower = sample; - upper = isOdd ? Math.Min(sample + 1, maximum) : sample; - upperWeight = isOdd ? 2 : 0; - } -} diff --git a/src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs new file mode 100644 index 0000000000..1af350164f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Color; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color; + +/// +/// Adapts reconstructed AV1 planes to the shared HEIF planar color pipeline. +/// +/// The native unsigned sample storage type. +internal struct Av1PlanarSampleBuffer : IHeifPlanarSampleBuffer + where TSample : unmanaged +{ + /// + /// The reconstructed AV1 frame containing the component planes. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The visible luma plane in byte-backed storage. + /// + private readonly Buffer2DRegion luma; + + /// + /// The visible blue-difference plane in byte-backed storage. + /// + private readonly Buffer2DRegion chromaBlue; + + /// + /// The visible red-difference plane in byte-backed storage. + /// + private readonly Buffer2DRegion chromaRed; + + /// + /// Initializes a new instance of the struct. + /// + /// The reconstructed AV1 frame. + public Av1PlanarSampleBuffer(Av1FrameBuffer frameBuffer) + { + this.frameBuffer = frameBuffer; + this.luma = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); + this.chromaBlue = this.IsMonochrome + ? default + : frameBuffer.DeriveBlockPointer(Av1Plane.U, this.ChromaSubsamplingX, this.ChromaSubsamplingY); + + this.chromaRed = this.IsMonochrome + ? default + : frameBuffer.DeriveBlockPointer(Av1Plane.V, this.ChromaSubsamplingX, this.ChromaSubsamplingY); + } + + /// + public readonly int Width => this.frameBuffer.Width; + + /// + public readonly int Height => this.frameBuffer.Height; + + /// + public readonly int LumaBitDepth => this.frameBuffer.BitDepth.GetBitCount(); + + /// + public readonly int ChromaBitDepth => this.frameBuffer.BitDepth.GetBitCount(); + + /// + public readonly bool IsMonochrome => this.frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; + + /// + public readonly int ChromaSubsamplingX => this.frameBuffer.ColorConfig.SubSamplingX ? 1 : 0; + + /// + public readonly int ChromaSubsamplingY => this.frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; + + /// + public readonly int ChromaPositionX + { + get + { + if (this.ChromaSubsamplingX == 0) + { + return 0; + } + + // AV1 4:2:2 chroma is centered horizontally. For 4:2:0, CSP_UNKNOWN is centered while the two + // explicitly positioned layouts are co-sited with the left luma sample. + bool isCentered = this.ChromaSubsamplingY == 0 + || this.frameBuffer.ColorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Unknown; + + return isCentered ? 1 : 0; + } + } + + /// + public readonly int ChromaPositionY + => this.ChromaSubsamplingY != 0 && this.frameBuffer.ColorConfig.ChromaSamplePosition != ObuChromoSamplePosition.Colocated ? 1 : 0; + + /// + public Span GetLumaRowSpan(int row) + { + if (typeof(TSample) == typeof(byte)) + { + return MemoryMarshal.Cast(this.luma.DangerousGetRowSpan(row)); + } + + return MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0)); + } + + /// + public Span GetChromaBlueRowSpan(int row) + { + if (typeof(TSample) == typeof(byte)) + { + return MemoryMarshal.Cast(this.chromaBlue.DangerousGetRowSpan(row)); + } + + return MemoryMarshal.Cast( + this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, row, this.ChromaSubsamplingX, this.ChromaSubsamplingY)); + } + + /// + public Span GetChromaRedRowSpan(int row) + { + if (typeof(TSample) == typeof(byte)) + { + return MemoryMarshal.Cast(this.chromaRed.DangerousGetRowSpan(row)); + } + + return MemoryMarshal.Cast( + this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, row, this.ChromaSubsamplingX, this.ChromaSubsamplingY)); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs b/src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs new file mode 100644 index 0000000000..3c3542db17 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Color; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.PixelFormats; +using static SixLabors.ImageSharp.Formats.Heif.Color.HeifColorConverterBase; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color; + +/// +/// Adapts AV1 color signaling and reconstructed planes to the shared HEIF color pipeline. +/// +internal static class Av1YuvConverter +{ + /// + /// Converts the reconstructed component planes to packed pixels. + /// + /// The destination pixel type. + /// The configuration used for allocation and pixel conversion. + /// The reconstructed AV1 frame. + /// The destination image frame. + public static void ConvertToRgb(Configuration configuration, Av1FrameBuffer frameBuffer, ImageFrame image) + where TPixel : unmanaged, IPixel + { + HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer, out HeifColorConversionMode mode); + if (frameBuffer.BitDepth == Av1BitDepth.EightBit) + { + Av1PlanarSampleBuffer buffer = new(frameBuffer); + HeifPlanarColorConverter.ConvertToRgb, byte, HeifByteSampleLoader>( + configuration, + buffer, + image, + in parameters, + mode); + + return; + } + + Av1PlanarSampleBuffer highBitDepthBuffer = new(frameBuffer); + HeifPlanarColorConverter.ConvertToRgb>( + configuration, + highBitDepthBuffer, + image, + in parameters, + mode); + } + + /// + /// Converts packed pixels to the configured monochrome or component planes used by the AV1 encoder. + /// + /// The source pixel type. + /// The configuration used for allocation and pixel conversion. + /// The source image frame. + /// The destination AV1 frame. + public static void ConvertFromRgb(Configuration configuration, ImageFrame image, Av1FrameBuffer frameBuffer) + where TPixel : unmanaged, IPixel + { + HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer, out HeifColorConversionMode mode); + if (frameBuffer.BitDepth == Av1BitDepth.EightBit) + { + Av1PlanarSampleBuffer buffer = new(frameBuffer); + HeifPlanarColorConverter.ConvertFromRgb, byte, HeifByteSampleStorer>( + configuration, + image, + buffer, + in parameters, + mode); + + return; + } + + Av1PlanarSampleBuffer highBitDepthBuffer = new(frameBuffer); + HeifPlanarColorConverter.ConvertFromRgb, ushort, HeifUShortSampleStorer>( + configuration, + image, + highBitDepthBuffer, + in parameters, + mode); + } + + /// + /// Resolves the H.273 conversion mode, matrix coefficients, and sample range for a frame. + /// + /// The AV1 frame containing the signaled color configuration. + /// The resolved conversion mode. + /// The resolved conversion parameters. + private static HeifColorConversionParameters GetConversionParameters(Av1FrameBuffer frameBuffer, out HeifColorConversionMode mode) + { + if (frameBuffer.ColorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Reserved) + { + throw new InvalidImageContentException("The reserved AV1 chroma sample position is invalid."); + } + + bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; + + return HeifColorConversionParameters.Create( + (CicpColorPrimaries)(byte)frameBuffer.ColorConfig.ColorPrimaries, + (CicpTransferCharacteristics)(byte)frameBuffer.ColorConfig.TransferCharacteristics, + (CicpMatrixCoefficients)(byte)frameBuffer.ColorConfig.MatrixCoefficients, + frameBuffer.ColorConfig.ColorRange, + frameBuffer.BitDepth.GetBitCount(), + frameBuffer.BitDepth.GetBitCount(), + isMonochrome, + frameBuffer.ColorFormat == Av1ColorFormat.Yuv444, + out mode); + } +} diff --git a/src/ImageSharp/Formats/Heif/Color/HeifColorConversionParameters.cs b/src/ImageSharp/Formats/Heif/Color/HeifColorConversionParameters.cs index 800ea90e9d..3ded58bb46 100644 --- a/src/ImageSharp/Formats/Heif/Color/HeifColorConversionParameters.cs +++ b/src/ImageSharp/Formats/Heif/Color/HeifColorConversionParameters.cs @@ -16,6 +16,8 @@ internal readonly struct HeifColorConversionParameters /// The red luma coefficient. /// The green luma coefficient. /// The blue luma coefficient. + /// The signaled H.273 matrix-coefficient code point. + /// Whether encoded components use their complete numeric range. /// The signaled transfer characteristics. /// The constant-luminance chroma scales. /// The encoded luma bias. @@ -31,6 +33,8 @@ internal readonly struct HeifColorConversionParameters float kr, float kg, float kb, + CicpMatrixCoefficients matrixCoefficients, + bool isFullRange, CicpTransferCharacteristics transferCharacteristics, in HeifConstantLuminanceScales constantLuminanceScales, float lumaBias, @@ -50,6 +54,8 @@ internal readonly struct HeifColorConversionParameters this.BlueChromaScale = 2F * (1F - kb); this.GreenRedChromaScale = 2F * kr * (1F - kr) / kg; this.GreenBlueChromaScale = 2F * kb * (1F - kb) / kg; + this.MatrixCoefficients = matrixCoefficients; + this.IsFullRange = isFullRange; this.TransferCharacteristics = transferCharacteristics; this.ConstantLuminanceScales = constantLuminanceScales; this.LumaBias = lumaBias; @@ -98,6 +104,16 @@ internal readonly struct HeifColorConversionParameters /// public float GreenBlueChromaScale { get; } + /// + /// Gets the signaled H.273 matrix-coefficient code point. + /// + public CicpMatrixCoefficients MatrixCoefficients { get; } + + /// + /// Gets a value indicating whether encoded components use their complete numeric range. + /// + public bool IsFullRange { get; } + /// /// Gets the signaled transfer characteristics. /// @@ -296,6 +312,8 @@ internal readonly struct HeifColorConversionParameters kr, kg, kb, + matrixCoefficients, + isFullRange, transferCharacteristics, in constantLuminanceScales, lumaBias, diff --git a/src/ImageSharp/Formats/Heif/Color/HeifPlanarColorConverter.cs b/src/ImageSharp/Formats/Heif/Color/HeifPlanarColorConverter.cs new file mode 100644 index 0000000000..d543601436 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Color/HeifPlanarColorConverter.cs @@ -0,0 +1,738 @@ +// 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.Memory; +using SixLabors.ImageSharp.PixelFormats; +using static SixLabors.ImageSharp.Formats.Heif.Color.HeifColorConverterBase; + +namespace SixLabors.ImageSharp.Formats.Heif.Color; + +/// +/// Converts between native HEIF component planes and packed ImageSharp pixels. +/// +internal static class HeifPlanarColorConverter +{ + /// + /// The largest value represented by an eight-bit packed RGB component. + /// + private const float ByteMaximum = byte.MaxValue; + + /// + /// The largest value represented by a 16-bit packed RGB component. + /// + private const float UShortMaximum = ushort.MaxValue; + + /// + /// Converts native unsigned 16-bit component storage to packed pixels and selects eligible exact integer kernels. + /// + /// The destination pixel type. + /// The codec adapter exposing the native component planes. + /// The configuration used for allocation and pixel conversion. + /// The native component-plane buffer. + /// The destination image frame. + /// The resolved H.273 conversion parameters. + /// The resolved H.273 conversion mode. + /// The horizontal luma-sample offset of the first converted pixel. + /// The vertical luma-sample offset of the first converted pixel. + public static void ConvertToRgb( + Configuration configuration, + TBuffer buffer, + ImageFrame image, + in HeifColorConversionParameters parameters, + HeifColorConversionMode mode, + int sourceX = 0, + int sourceY = 0) + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + { + if (HeifYuv420ToRgb8Converter.IsSupported( + buffer.ChromaSubsamplingX, + buffer.ChromaSubsamplingY, + buffer.LumaBitDepth, + buffer.ChromaBitDepth, + parameters.IsFullRange, + parameters.MatrixCoefficients, + mode)) + { + // The fixed-point operator preserves the exact code-value rounding used by the verified eight-bit + // presentation path. Selection belongs here so no codec can acquire a private color-conversion route. + HeifYuv420ToRgb8Converter.Convert(configuration, buffer, image, in parameters, sourceX, sourceY); + return; + } + + ConvertToRgb( + configuration, + buffer, + image, + in parameters, + mode, + sourceX, + sourceY); + } + + /// + /// Converts native component planes to packed pixels. + /// + /// The destination pixel type. + /// The codec adapter exposing the native component planes. + /// The native unsigned sample storage type. + /// The SIMD widening operations for the sample type. + /// The configuration used for allocation and pixel conversion. + /// The native component-plane buffer. + /// The destination image frame. + /// The resolved H.273 conversion parameters. + /// The resolved H.273 conversion mode. + /// The horizontal luma-sample offset of the first converted pixel. + /// The vertical luma-sample offset of the first converted pixel. + public static void ConvertToRgb( + Configuration configuration, + TBuffer buffer, + ImageFrame image, + in HeifColorConversionParameters parameters, + HeifColorConversionMode mode, + int sourceX = 0, + int sourceY = 0) + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + where TSample : unmanaged + where TLoader : struct, IHeifSampleLoader + { + HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, buffer.IsMonochrome); + YuvToRgbRowConverter converter = new( + configuration, + buffer, + image, + colorConverter, + sourceX, + sourceY); + + using IMemoryOwner scratchOwner = configuration.MemoryAllocator.Allocate(converter.BufferLength); + Span scratch = scratchOwner.GetSpan(); + for (int y = 0; y < image.Height; y++) + { + converter.Convert(y, scratch); + } + } + + /// + /// Converts packed pixels to native component planes. + /// + /// The source pixel type. + /// The codec adapter exposing the native component planes. + /// The native unsigned sample storage type. + /// The SIMD narrowing and storage operations for the sample type. + /// The configuration used for allocation and pixel conversion. + /// The source image frame. + /// The destination component-plane buffer. + /// The resolved H.273 conversion parameters. + /// The resolved H.273 conversion mode. + public static void ConvertFromRgb( + Configuration configuration, + ImageFrame image, + TBuffer buffer, + in HeifColorConversionParameters parameters, + HeifColorConversionMode mode) + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + where TSample : unmanaged + where TStorer : struct, IHeifSampleStorer + { + HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, buffer.IsMonochrome); + RgbToYuvRowConverter converter = new( + configuration, + buffer, + image, + colorConverter, + in parameters); + + using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(converter.ComponentBufferLength); + Span components = componentOwner.GetSpan(); + if (converter.UsesByteInput) + { + converter.Convert(Span.Empty, components); + return; + } + + using IMemoryOwner packedOwner = configuration.MemoryAllocator.Allocate(image.Width); + converter.Convert(packedOwner.GetSpan()[..image.Width], components); + } + + /// + /// Resolves the two chroma rows and quarter-sample weight surrounding a luma row. + /// + /// The luma row coordinate. + /// The vertical chroma subsampling shift. + /// The chroma offset in half-luma-sample units. + /// The last available chroma row. + /// The lower chroma row. + /// The upper chroma row. + /// The upper-row weight with a denominator of four. + 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; + } + + // Multiplication by two expresses the luma coordinate in half-sample units, while division by four + // addresses the subsampled plane. Floor division is required before the first centered chroma sample. + 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; + } + + /// + /// Converts one native component row using pooled planar and packed-pixel storage. + /// + /// The destination pixel type. + /// The codec adapter exposing the native component planes. + /// The native unsigned sample storage type. + /// The SIMD widening operations for the sample type. + private struct YuvToRgbRowConverter + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + where TSample : unmanaged + where TLoader : struct, IHeifSampleLoader + { + /// + /// The configuration used for packed-pixel conversion. + /// + private readonly Configuration configuration; + + /// + /// The codec adapter exposing the reconstructed component planes. + /// + private TBuffer buffer; + + /// + /// The destination image frame. + /// + private readonly ImageFrame image; + + /// + /// The selected H.273 color converter. + /// + private readonly HeifColorConverterBase colorConverter; + + /// + /// The horizontal luma-sample offset of the output window. + /// + private readonly int sourceX; + + /// + /// The vertical luma-sample offset of the output window. + /// + private readonly int sourceY; + + /// + /// The full coded luma-plane width. + /// + private readonly int bufferWidth; + + /// + /// The chroma-plane width in samples. + /// + private readonly int chromaWidth; + + /// + /// The chroma-plane height in samples. + /// + private readonly int chromaHeight; + + /// + /// The horizontal chroma subsampling shift. + /// + private readonly int subsamplingX; + + /// + /// The vertical chroma subsampling shift. + /// + private readonly int subsamplingY; + + /// + /// The horizontal chroma position in half-luma-sample units. + /// + private readonly int chromaPositionX; + + /// + /// The vertical chroma position in half-luma-sample units. + /// + private readonly int chromaPositionY; + + /// + /// Whether the buffer contains only the luma plane. + /// + private readonly bool isMonochrome; + + /// + /// Whether the destination uses JPEG-compatible eight-bit RGB plane packing. + /// + private readonly bool usesBytePacking; + + /// + /// Whether chroma reconstruction must retain the complete coded row before selecting the output window. + /// + private readonly bool reconstructCompleteRow; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration used for pixel conversion. + /// The codec adapter exposing the reconstructed component planes. + /// The destination image frame. + /// The selected H.273 color converter. + /// The horizontal luma-sample offset of the output window. + /// The vertical luma-sample offset of the output window. + public YuvToRgbRowConverter( + Configuration configuration, + TBuffer buffer, + ImageFrame image, + HeifColorConverterBase colorConverter, + int sourceX, + int sourceY) + { + this.configuration = configuration; + this.buffer = buffer; + this.image = image; + this.colorConverter = colorConverter; + this.sourceX = sourceX; + this.sourceY = sourceY; + this.bufferWidth = buffer.Width; + this.subsamplingX = buffer.ChromaSubsamplingX; + this.subsamplingY = buffer.ChromaSubsamplingY; + this.chromaPositionX = buffer.ChromaPositionX; + this.chromaPositionY = buffer.ChromaPositionY; + this.isMonochrome = buffer.IsMonochrome; + this.usesBytePacking = buffer.LumaBitDepth == 8 && (buffer.IsMonochrome || buffer.ChromaBitDepth == 8); + this.chromaWidth = (buffer.Width + (1 << this.subsamplingX) - 1) >> this.subsamplingX; + this.chromaHeight = (buffer.Height + (1 << this.subsamplingY) - 1) >> this.subsamplingY; + this.reconstructCompleteRow = sourceX != 0 || image.Width != buffer.Width; + } + + /// + /// Gets a value indicating whether the destination uses JPEG-compatible eight-bit RGB plane packing. + /// + public readonly bool UsesBytePacking => this.usesBytePacking; + + /// + /// Gets the number of float elements required by the reusable row buffer. + /// + public readonly int BufferLength + { + get + { + int componentLength = this.image.Width * 3; + if (!this.isMonochrome && this.subsamplingX != 0) + { + // Full-image conversion reconstructs directly into the component rows. Cropped conversion + // retains the complete coded row so interpolation phase is preserved at the window boundary. + componentLength += (this.chromaWidth * 2) + (this.reconstructCompleteRow ? this.bufferWidth : 0); + } + + int packedRowCount = this.UsesBytePacking ? 1 : 2; + return componentLength + (this.image.Width * packedRowCount); + } + } + + /// + /// Converts one reconstructed row to packed pixels. + /// + /// The zero-based output row. + /// The reusable pooled row buffer. + public void Convert(int y, Span scratch) + { + int width = this.image.Width; + Span red = scratch[..width]; + Span green = scratch.Slice(width, width); + Span blue = scratch.Slice(width * 2, width); + int sourceY = y + this.sourceY; + ReadOnlySpan luma = this.buffer.GetLumaRowSpan(sourceY).Slice(this.sourceX, width); + ConvertSamplesToFloat(luma, red); + + int packedOffset = width * 3; + if (!this.isMonochrome) + { + GetChromaCoordinates( + sourceY, + this.subsamplingY, + this.chromaPositionY, + this.chromaHeight - 1, + out int y0, + out int y1, + out int y1Weight); + + ReadOnlySpan cb0 = this.buffer.GetChromaBlueRowSpan(y0); + ReadOnlySpan cb1 = this.buffer.GetChromaBlueRowSpan(y1); + ReadOnlySpan cr0 = this.buffer.GetChromaRedRowSpan(y0); + ReadOnlySpan cr1 = this.buffer.GetChromaRedRowSpan(y1); + if (this.subsamplingX == 0) + { + ConvertSamplesToFloat(cb0.Slice(this.sourceX, width), green); + ConvertSamplesToFloat(cr0.Slice(this.sourceX, width), blue); + } + else + { + Span chroma0 = scratch.Slice(packedOffset, this.chromaWidth); + Span chroma1 = scratch.Slice(packedOffset + this.chromaWidth, this.chromaWidth); + Span reconstructed = this.reconstructCompleteRow + ? scratch.Slice(packedOffset + (this.chromaWidth * 2), this.bufferWidth) + : green; + + bool isCenteredX = this.chromaPositionX == 1; + + ReconstructChromaRow( + cb0, + cb1, + y1Weight, + this.subsamplingX, + isCenteredX, + reconstructed, + chroma0, + chroma1); + + if (this.reconstructCompleteRow) + { + reconstructed.Slice(this.sourceX, width).CopyTo(green); + } + + reconstructed = this.reconstructCompleteRow ? reconstructed : blue; + ReconstructChromaRow( + cr0, + cr1, + y1Weight, + this.subsamplingX, + isCenteredX, + reconstructed, + chroma0, + chroma1); + + if (this.reconstructCompleteRow) + { + reconstructed.Slice(this.sourceX, width).CopyTo(blue); + } + + packedOffset += (this.chromaWidth * 2) + (this.reconstructCompleteRow ? this.bufferWidth : 0); + } + } + + this.colorConverter.ConvertToRgbInPlace(red, green, blue); + Span destination = this.image.PixelBuffer.DangerousGetRowSpan(y); + Span 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 rather than a HEIF-codec-specific per-pixel implementation. + Span byteStorage = MemoryMarshal.AsBytes(packedStorage)[..(width * 3)]; + Span redBytes = byteStorage[..width]; + Span greenBytes = byteStorage.Slice(width, width); + Span blueBytes = byteStorage.Slice(width * 2, width); + SimdUtils.NormalizedFloatToByteSaturate(red, redBytes); + SimdUtils.NormalizedFloatToByteSaturate(green, greenBytes); + SimdUtils.NormalizedFloatToByteSaturate(blue, blueBytes); + + PixelOperations.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, destination); + return; + } + + Span packed = MemoryMarshal.Cast(packedStorage)[..width]; + PackRgba64(red, green, blue, packed); + PixelOperations.Instance.FromRgba64(this.configuration, packed, destination); + } + } + + /// + /// Converts packed image rows to native component planes using pooled planar storage. + /// + /// The source pixel type. + /// The codec adapter exposing the native component planes. + /// The native unsigned sample storage type. + /// The SIMD narrowing and storage operations for the sample type. + private struct RgbToYuvRowConverter + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + where TSample : unmanaged + where TStorer : struct, IHeifSampleStorer + { + /// + /// The configuration used for packed-pixel conversion. + /// + private readonly Configuration configuration; + + /// + /// The codec adapter exposing the destination component planes. + /// + private TBuffer buffer; + + /// + /// The source image frame. + /// + private readonly ImageFrame image; + + /// + /// The selected H.273 color converter. + /// + private readonly HeifColorConverterBase colorConverter; + + /// + /// The largest encoded luma sample value. + /// + private readonly float lumaMaximum; + + /// + /// The largest encoded chroma sample value. + /// + private readonly float chromaMaximum; + + /// + /// The horizontal chroma subsampling shift. + /// + private readonly int subsamplingX; + + /// + /// The vertical chroma subsampling shift. + /// + private readonly int subsamplingY; + + /// + /// The horizontal chroma position in half-luma-sample units. + /// + private readonly int chromaPositionX; + + /// + /// The vertical chroma position in half-luma-sample units. + /// + private readonly int chromaPositionY; + + /// + /// Whether the destination buffer contains only the luma plane. + /// + private readonly bool isMonochrome; + + /// + /// Whether source pixels use JPEG-compatible eight-bit RGB plane unpacking. + /// + private readonly bool usesByteInput; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration used for pixel conversion. + /// The codec adapter exposing the destination component planes. + /// The source image frame. + /// The selected H.273 color converter. + /// The resolved H.273 component ranges. + public RgbToYuvRowConverter( + Configuration configuration, + TBuffer buffer, + ImageFrame image, + HeifColorConverterBase colorConverter, + in HeifColorConversionParameters parameters) + { + this.configuration = configuration; + this.buffer = buffer; + this.image = image; + this.colorConverter = colorConverter; + this.lumaMaximum = parameters.LumaSampleMaximum; + this.chromaMaximum = parameters.ChromaSampleMaximum; + this.subsamplingX = buffer.ChromaSubsamplingX; + this.subsamplingY = buffer.ChromaSubsamplingY; + this.chromaPositionX = buffer.ChromaPositionX; + this.chromaPositionY = buffer.ChromaPositionY; + this.isMonochrome = buffer.IsMonochrome; + this.usesByteInput = buffer.LumaBitDepth == 8 && (buffer.IsMonochrome || buffer.ChromaBitDepth == 8); + } + + /// + /// Gets a value indicating whether source pixels use JPEG-compatible eight-bit RGB plane unpacking. + /// + public readonly bool UsesByteInput => this.usesByteInput; + + /// + /// Gets the number of float elements required by the reusable component buffer. + /// + public readonly int ComponentBufferLength => this.image.Width * (this.subsamplingY == 0 || this.isMonochrome ? 3 : 6); + + /// + /// Converts every packed source row to the destination component planes. + /// + /// The reusable high-bit-depth RGB staging row. + /// The reusable planar component buffer. + public void Convert(Span packed, Span components) + { + int width = this.image.Width; + Span luma0 = components[..width]; + Span blue0 = components.Slice(width, width); + Span red0 = components.Slice(width * 2, width); + if (this.subsamplingY == 0) + { + for (int y = 0; y < this.image.Height; y++) + { + this.ConvertSourceRow(y, packed, luma0, blue0, red0); + WriteSamples( + luma0, + this.buffer.GetLumaRowSpan(y), + this.colorConverter.LumaScale, + this.colorConverter.LumaBias, + this.lumaMaximum); + + if (!this.isMonochrome) + { + this.WriteChromaRows(y, blue0, red0, Span.Empty, Span.Empty, 0F); + } + } + + return; + } + + Span luma1 = this.isMonochrome ? luma0 : components.Slice(width * 3, width); + Span blue1 = this.isMonochrome ? blue0 : components.Slice(width * 4, width); + Span red1 = this.isMonochrome ? red0 : components.Slice(width * 5, width); + int chromaHeight = (this.image.Height + 1) >> 1; + for (int destinationY = 0; destinationY < chromaHeight; destinationY++) + { + // A vertically subsampled chroma row is owned by one two-row luma cell. Processing that cell as a + // unit removes per-row state and lets the selected chroma position choose or average the two rows. + int sourceY = destinationY << 1; + this.ConvertSourceRow(sourceY, packed, luma0, blue0, red0); + WriteSamples( + luma0, + this.buffer.GetLumaRowSpan(sourceY), + this.colorConverter.LumaScale, + this.colorConverter.LumaBias, + this.lumaMaximum); + + bool hasSecondRow = sourceY + 1 < this.image.Height; + if (hasSecondRow) + { + this.ConvertSourceRow(sourceY + 1, packed, luma1, blue1, red1); + WriteSamples( + luma1, + this.buffer.GetLumaRowSpan(sourceY + 1), + this.colorConverter.LumaScale, + this.colorConverter.LumaBias, + this.lumaMaximum); + } + + if (this.isMonochrome) + { + continue; + } + + if (this.chromaPositionY == 0 || !hasSecondRow) + { + this.WriteChromaRows(destinationY, blue0, red0, Span.Empty, Span.Empty, 0F); + } + else if (this.chromaPositionY == 1) + { + this.WriteChromaRows(destinationY, blue0, red0, blue1, red1, 0.5F); + } + else + { + this.WriteChromaRows(destinationY, blue1, red1, Span.Empty, Span.Empty, 0F); + } + } + } + + /// + /// Converts one packed source row into normalized planar components. + /// + /// The source row index. + /// The high-bit-depth RGB staging row. + /// The destination luma or first component values. + /// The destination blue-difference or second component values. + /// The destination red-difference or third component values. + private void ConvertSourceRow(int y, Span packed, Span luma, Span chromaBlue, Span chromaRed) + { + ReadOnlySpan 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 component rows in place. + PixelOperations.Instance.UnpackIntoRgbPlanes(luma, chromaBlue, chromaRed, source); + this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, ByteMaximum); + return; + } + + PixelOperations.Instance.ToRgb48(this.configuration, source, packed); + DeinterleaveRgb48(packed, luma, chromaBlue, chromaRed); + this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, UShortMaximum); + } + + /// + /// Filters and writes one pair of chroma component rows. + /// + /// The destination chroma row. + /// The first blue-difference source row. + /// The first red-difference source row. + /// The optional second blue-difference source row. + /// The optional second red-difference source row. + /// The second-row contribution. + private void WriteChromaRows( + int destinationY, + ReadOnlySpan blue0, + ReadOnlySpan red0, + ReadOnlySpan blue1, + ReadOnlySpan red1, + float row1Weight) + { + Span blueDestination = this.buffer.GetChromaBlueRowSpan(destinationY); + Span redDestination = this.buffer.GetChromaRedRowSpan(destinationY); + if (this.subsamplingX == 0) + { + WriteSamples( + blue0, + blueDestination, + this.colorConverter.ChromaScale, + this.colorConverter.ChromaBias, + this.chromaMaximum); + + WriteSamples( + red0, + redDestination, + this.colorConverter.ChromaScale, + this.colorConverter.ChromaBias, + this.chromaMaximum); + + return; + } + + bool isCenteredX = this.chromaPositionX == 1; + WriteSubsampledSamples( + blue0, + blue1, + blueDestination, + isCenteredX, + row1Weight, + this.colorConverter.ChromaScale, + this.colorConverter.ChromaBias, + this.chromaMaximum); + + WriteSubsampledSamples( + red0, + red1, + redDestination, + isCenteredX, + row1Weight, + this.colorConverter.ChromaScale, + this.colorConverter.ChromaBias, + this.chromaMaximum); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Color/HeifYuv420ToRgb8Converter.cs b/src/ImageSharp/Formats/Heif/Color/HeifYuv420ToRgb8Converter.cs index 0aea19622d..be231a77be 100644 --- a/src/ImageSharp/Formats/Heif/Color/HeifYuv420ToRgb8Converter.cs +++ b/src/ImageSharp/Formats/Heif/Color/HeifYuv420ToRgb8Converter.cs @@ -60,22 +60,22 @@ internal static partial class HeifYuv420ToRgb8Converter /// Converts supported HEIF component planes to packed pixels using integer SIMD with a scalar tail. /// /// The destination pixel type. - /// The codec adapter that exposes reconstructed component rows. + /// The codec adapter that exposes reconstructed component rows. /// The configuration used for allocation and pixel conversion. - /// The reconstructed component-plane source. + /// The reconstructed component-plane buffer. /// The destination image frame. /// The resolved H.273 conversion parameters. /// The horizontal luma-sample offset of the output window. /// The vertical luma-sample offset of the output window. - public static void Convert( + public static void Convert( Configuration configuration, - TSource source, + TBuffer buffer, ImageFrame image, in HeifColorConversionParameters parameters, int sourceX, int sourceY) where TPixel : unmanaged, IPixel - where TSource : struct, IHeifYuv420ToRgb8Source + where TBuffer : struct, IHeifPlanarSampleBuffer { ConversionParameters conversionParameters = new(in parameters); using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(image.Width * 3); @@ -84,7 +84,7 @@ internal static partial class HeifYuv420ToRgb8Converter Span green = components.Slice(image.Width, image.Width); Span blue = components.Slice(image.Width * 2, image.Width); - // The value-type source closes the row-access contract at the call site. Constrained calls are therefore + // The value-type buffer closes the row-access contract at the call site. Constrained calls are therefore // devirtualized without boxing while keeping codec-specific buffer ownership outside the color pipeline. for (int y = 0; y < image.Height; y++) { @@ -92,9 +92,9 @@ internal static partial class HeifYuv420ToRgb8Converter // The codec boundary validates 4:2:0 crop offsets in complete chroma-sample units. Each native chroma // sample therefore covers one 2x2 luma cell without an alignment branch in the SIMD loop. - ReadOnlySpan luma = source.GetLumaRow(lumaY).Slice(sourceX, image.Width); - ReadOnlySpan chromaBlue = source.GetChromaBlueRow(lumaY >> 1).Slice(sourceX >> 1); - ReadOnlySpan chromaRed = source.GetChromaRedRow(lumaY >> 1).Slice(sourceX >> 1); + ReadOnlySpan luma = buffer.GetLumaRowSpan(lumaY).Slice(sourceX, image.Width); + ReadOnlySpan chromaBlue = buffer.GetChromaBlueRowSpan(lumaY >> 1).Slice(sourceX >> 1); + ReadOnlySpan chromaRed = buffer.GetChromaRedRowSpan(lumaY >> 1).Slice(sourceX >> 1); ConvertRow(luma, chromaBlue, chromaRed, red, green, blue, in conversionParameters); diff --git a/src/ImageSharp/Formats/Heif/Color/IHeifPlanarSampleBuffer.cs b/src/ImageSharp/Formats/Heif/Color/IHeifPlanarSampleBuffer.cs new file mode 100644 index 0000000000..be597fbef6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Color/IHeifPlanarSampleBuffer.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Color; + +/// +/// Exposes the native planar sample layout shared by HEIF image codecs. +/// +/// The native unsigned sample storage type. +internal interface IHeifPlanarSampleBuffer + where TSample : unmanaged +{ + /// + /// Gets the full luma-plane width in samples. + /// + public int Width { get; } + + /// + /// Gets the full luma-plane height in samples. + /// + public int Height { get; } + + /// + /// Gets the luma sample precision in bits. + /// + public int LumaBitDepth { get; } + + /// + /// Gets the chroma sample precision in bits. + /// + public int ChromaBitDepth { get; } + + /// + /// Gets a value indicating whether the buffer contains only the luma plane. + /// + public bool IsMonochrome { get; } + + /// + /// Gets the horizontal chroma subsampling shift. + /// + public int ChromaSubsamplingX { get; } + + /// + /// Gets the vertical chroma subsampling shift. + /// + public int ChromaSubsamplingY { get; } + + /// + /// Gets the horizontal chroma position in half-luma-sample units. + /// + public int ChromaPositionX { get; } + + /// + /// Gets the vertical chroma position in half-luma-sample units. + /// + public int ChromaPositionY { get; } + + /// + /// Gets one complete writable row from the luma, green, or intensity plane. + /// + /// The zero-based row index in plane samples. + /// The native luma, green, or intensity samples. + public Span GetLumaRowSpan(int row); + + /// + /// Gets one complete writable row from the blue-difference, blue, or first opponent-color plane. + /// + /// The zero-based row index in plane samples. + /// The native blue-difference, blue, or first opponent-color samples. + public Span GetChromaBlueRowSpan(int row); + + /// + /// Gets one complete writable row from the red-difference, red, or second opponent-color plane. + /// + /// The zero-based row index in plane samples. + /// The native red-difference, red, or second opponent-color samples. + public Span GetChromaRedRowSpan(int row); +} diff --git a/src/ImageSharp/Formats/Heif/Color/IHeifYuv420ToRgb8Source.cs b/src/ImageSharp/Formats/Heif/Color/IHeifYuv420ToRgb8Source.cs deleted file mode 100644 index 238e40945d..0000000000 --- a/src/ImageSharp/Formats/Heif/Color/IHeifYuv420ToRgb8Source.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Heif.Color; - -/// -/// Exposes reconstructed eight-bit 4:2:0 component rows to the shared fixed-point HEIF converter. -/// -internal interface IHeifYuv420ToRgb8Source -{ - /// - /// Gets one complete luma row in native sample storage. - /// - /// The zero-based luma row. - /// The reconstructed luma samples. - public ReadOnlySpan GetLumaRow(int row); - - /// - /// Gets one complete blue-difference chroma row in native sample storage. - /// - /// The zero-based chroma row. - /// The reconstructed blue-difference samples. - public ReadOnlySpan GetChromaBlueRow(int row); - - /// - /// Gets one complete red-difference chroma row in native sample storage. - /// - /// The zero-based chroma row. - /// The reconstructed red-difference samples. - public ReadOnlySpan GetChromaRedRow(int row); -} diff --git a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcPlanarSampleBuffer.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcPlanarSampleBuffer.cs new file mode 100644 index 0000000000..6244636164 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcPlanarSampleBuffer.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Color; + +namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color; + +/// +/// Adapts reconstructed HEVC planes to the shared HEIF planar color pipeline. +/// +internal struct HevcPlanarSampleBuffer : IHeifPlanarSampleBuffer +{ + /// + /// The reconstructed HEVC picture containing the component planes. + /// + private readonly HevcPictureBuffer picture; + + /// + /// The progressive-frame 4:2:0 chroma sample location. + /// + private readonly HevcChromaSampleLocation chromaSampleLocation; + + /// + /// Initializes a new instance of the struct. + /// + /// The reconstructed HEVC picture. + /// The progressive-frame 4:2:0 chroma sample location. + public HevcPlanarSampleBuffer(HevcPictureBuffer picture, HevcChromaSampleLocation chromaSampleLocation) + { + this.picture = picture; + this.chromaSampleLocation = chromaSampleLocation; + } + + /// + /// Gets the horizontal offset in half-luma-sample units for each HEVC 4:2:0 chroma-location code. + /// + private static ReadOnlySpan ChromaLocationX => [0, 1, 0, 1, 0, 1]; + + /// + /// Gets the vertical offset in half-luma-sample units for each HEVC 4:2:0 chroma-location code. + /// + private static ReadOnlySpan ChromaLocationY => [1, 1, 0, 0, 2, 2]; + + /// + public readonly int Width => this.picture.Width; + + /// + public readonly int Height => this.picture.Height; + + /// + public readonly int LumaBitDepth => this.picture.BitDepthLuma; + + /// + public readonly int ChromaBitDepth => this.picture.BitDepthChroma; + + /// + public readonly bool IsMonochrome => this.picture.ChromaFormat == 0; + + /// + public readonly int ChromaSubsamplingX => this.picture.GetSubsamplingX(HevcPlane.Cb); + + /// + public readonly int ChromaSubsamplingY => this.picture.GetSubsamplingY(HevcPlane.Cb); + + /// + public readonly int ChromaPositionX + => this.picture.ChromaFormat == 1 && !this.picture.SeparateColorPlane ? ChromaLocationX[(int)this.chromaSampleLocation] : 0; + + /// + public readonly int ChromaPositionY + => this.picture.ChromaFormat == 1 && !this.picture.SeparateColorPlane ? ChromaLocationY[(int)this.chromaSampleLocation] : 0; + + /// + public Span GetLumaRowSpan(int row) => this.picture.GetRowSpan(HevcPlane.Y, row); + + /// + public Span GetChromaBlueRowSpan(int row) => this.picture.GetRowSpan(HevcPlane.Cb, row); + + /// + public Span GetChromaRedRowSpan(int row) => this.picture.GetRowSpan(HevcPlane.Cr, row); +} diff --git a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Source.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Source.cs deleted file mode 100644 index 9598ad136a..0000000000 --- a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuv420ToRgb8Source.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Heif.Color; - -namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color; - -/// -/// Adapts reconstructed HEVC planes to the shared fixed-point HEIF color converter. -/// -internal readonly struct HevcYuv420ToRgb8Source : IHeifYuv420ToRgb8Source -{ - /// - /// The reconstructed HEVC picture containing the component planes. - /// - private readonly HevcPictureBuffer picture; - - /// - /// Initializes a new instance of the struct. - /// - /// The reconstructed HEVC picture. - public HevcYuv420ToRgb8Source(HevcPictureBuffer picture) => this.picture = picture; - - /// - public ReadOnlySpan GetLumaRow(int row) => this.picture.GetRowSpan(HevcPlane.Y, row); - - /// - public ReadOnlySpan GetChromaBlueRow(int row) => this.picture.GetRowSpan(HevcPlane.Cb, row); - - /// - public ReadOnlySpan GetChromaRedRow(int row) => this.picture.GetRowSpan(HevcPlane.Cr, row); -} diff --git a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs index d822b03181..510c1dc612 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/Color/HevcYuvConverter.cs @@ -1,12 +1,7 @@ // 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; @@ -14,30 +9,10 @@ using static SixLabors.ImageSharp.Formats.Heif.Color.HeifColorConverterBase; namespace SixLabors.ImageSharp.Formats.Heif.Hevc.Color; /// -/// Converts between reconstructed HEVC component planes and packed ImageSharp pixels. +/// Adapts HEVC color signaling and reconstructed planes to the shared HEIF color pipeline. /// -internal static partial class HevcYuvConverter +internal static class HevcYuvConverter { - /// - /// The largest value represented by an eight-bit packed RGB component. - /// - private const float ByteMaximum = byte.MaxValue; - - /// - /// The largest value represented by a 16-bit packed RGB component. - /// - private const float UShortMaximum = ushort.MaxValue; - - /// - /// Gets the horizontal offset in half-luma-sample units for each HEVC 4:2:0 chroma-location code. - /// - private static ReadOnlySpan ChromaLocationX => [0, 1, 0, 1, 0, 1]; - - /// - /// Gets the vertical offset in half-luma-sample units for each HEVC 4:2:0 chroma-location code. - /// - private static ReadOnlySpan ChromaLocationY => [1, 1, 0, 0, 2, 2]; - /// /// Converts reconstructed HEVC component planes to packed pixels. /// @@ -60,42 +35,15 @@ internal static partial class HevcYuvConverter where TPixel : unmanaged, IPixel { HeifColorConversionParameters parameters = GetConversionParameters(picture, colorProfile, out HeifColorConversionMode mode); - - // The common full-range eight-bit path remains in the integer sample domain. The codec adapter exposes - // only native rows; shared HEIF code owns coefficient arithmetic, SIMD traversal, pooling, and packing. - if (HeifYuv420ToRgb8Converter.IsSupported( - picture.GetSubsamplingX(HevcPlane.Cb), - picture.GetSubsamplingY(HevcPlane.Cb), - picture.BitDepthLuma, - picture.BitDepthChroma, - colorProfile.FullRange, - colorProfile.MatrixCoefficients, - mode)) - { - HevcYuv420ToRgb8Source source = new(picture); - HeifYuv420ToRgb8Converter.Convert(configuration, source, image, in parameters, sourceX, sourceY); - return; - } - - HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, picture.ChromaFormat == 0); - YuvToRgbRowConverter converter = new(configuration, picture, image, colorConverter, chromaSampleLocation, sourceX, sourceY); - using IMemoryOwner scratchOwner = configuration.MemoryAllocator.Allocate(converter.BufferLength); - Span scratch = scratchOwner.GetSpan(); - - if (converter.UsesBytePacking) - { - for (int y = 0; y < image.Height; y++) - { - converter.Convert(y, scratch); - } - - return; - } - - for (int y = 0; y < image.Height; y++) - { - converter.Convert(y, scratch); - } + HevcPlanarSampleBuffer buffer = new(picture, chromaSampleLocation); + HeifPlanarColorConverter.ConvertToRgb( + configuration, + buffer, + image, + in parameters, + mode, + sourceX, + sourceY); } /// @@ -116,19 +64,13 @@ internal static partial class HevcYuvConverter where TPixel : unmanaged, IPixel { HeifColorConversionParameters parameters = GetConversionParameters(picture, colorProfile, out HeifColorConversionMode mode); - HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, picture.ChromaFormat == 0); - RgbToYuvRowConverter converter = new(configuration, picture, image, colorConverter, chromaSampleLocation, in parameters); - using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(converter.ComponentBufferLength); - Span components = componentOwner.GetSpan(); - - if (converter.UsesByteInput) - { - converter.Convert(Span.Empty, components); - return; - } - - using IMemoryOwner packedOwner = configuration.MemoryAllocator.Allocate(image.Width); - converter.Convert(packedOwner.GetSpan()[..image.Width], components); + HevcPlanarSampleBuffer buffer = new(picture, chromaSampleLocation); + HeifPlanarColorConverter.ConvertFromRgb( + configuration, + image, + buffer, + in parameters, + mode); } /// @@ -152,507 +94,4 @@ internal static partial class HevcYuvConverter picture.ChromaFormat == 0, picture.ChromaFormat == 3, out mode); - - /// - /// Resolves the two chroma rows and quarter-sample weight surrounding a luma row. - /// - /// The luma row coordinate. - /// The vertical chroma subsampling shift. - /// The chroma offset in half-luma-sample units. - /// The last available chroma row. - /// The lower chroma row. - /// The upper chroma row. - /// The upper-row weight with a denominator of four. - 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; - } - - /// - /// Resolves the luma-relative chroma offsets for the selected plane layout. - /// - /// The HEVC picture layout. - /// The signaled 4:2:0 location. - /// The horizontal offset in half-luma-sample units. - /// The vertical offset in half-luma-sample units. - 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]; - } - - /// - /// Converts one reconstructed HEVC row using pooled planar and packed-pixel storage. - /// - /// The destination pixel type. - private readonly struct YuvToRgbRowConverter - where TPixel : unmanaged, IPixel - { - /// - /// The configuration used for packed-pixel conversion. - /// - private readonly Configuration configuration; - - /// - /// The reconstructed HEVC picture. - /// - private readonly HevcPictureBuffer picture; - - /// - /// The destination image frame. - /// - private readonly ImageFrame image; - - /// - /// The selected H.273 color converter. - /// - private readonly HeifColorConverterBase colorConverter; - - /// - /// The horizontal chroma offset in half-luma-sample units. - /// - private readonly int horizontalPosition; - - /// - /// The vertical chroma offset in half-luma-sample units. - /// - private readonly int verticalPosition; - - /// - /// The horizontal chroma subsampling shift. - /// - private readonly int subX; - - /// - /// The vertical chroma subsampling shift. - /// - private readonly int subY; - - /// - /// The horizontal luma-sample offset of the output window. - /// - private readonly int sourceX; - - /// - /// The vertical luma-sample offset of the output window. - /// - private readonly int sourceY; - - /// - /// Initializes a new instance of the struct. - /// - /// The configuration used for pixel conversion. - /// The reconstructed HEVC picture. - /// The destination image frame. - /// The selected H.273 color converter. - /// The progressive-frame 4:2:0 chroma sample location. - /// The horizontal luma-sample offset of the output window. - /// The vertical luma-sample offset of the output window. - public YuvToRgbRowConverter( - Configuration configuration, - HevcPictureBuffer picture, - ImageFrame image, - HeifColorConverterBase colorConverter, - HevcChromaSampleLocation chromaSampleLocation, - int sourceX, - int sourceY) - { - this.configuration = configuration; - this.picture = picture; - this.image = image; - this.colorConverter = colorConverter; - this.subX = picture.GetSubsamplingX(HevcPlane.Cb); - this.subY = picture.GetSubsamplingY(HevcPlane.Cb); - this.sourceX = sourceX; - this.sourceY = sourceY; - GetChromaPosition(picture, chromaSampleLocation, out this.horizontalPosition, out this.verticalPosition); - } - - /// - /// Gets a value indicating whether the destination uses JPEG-compatible eight-bit RGB plane packing. - /// - public bool UsesBytePacking => this.picture.BitDepthLuma == 8 && (this.picture.ChromaFormat == 0 || this.picture.BitDepthChroma == 8); - - /// - /// Gets the number of float elements required by the reusable row buffer. - /// - public int BufferLength - { - get - { - int componentLength = this.image.Width * 3; - if (this.picture.ChromaFormat != 0 && this.subX != 0) - { - // Cropped output can begin between subsampled chroma positions. Reconstructing one complete - // coded-width row preserves the edge interpolation before selecting the visible window. - componentLength += this.picture.Width + (this.picture.GetWidth(HevcPlane.Cb) * 2); - } - - int packedRowCount = this.UsesBytePacking ? 1 : 2; - return componentLength + (this.image.Width * packedRowCount); - } - } - - /// - /// Converts one reconstructed row to packed pixels. - /// - /// The zero-based luma row. - /// The reusable pooled row buffer. - public void Convert(int y, Span scratch) - { - int width = this.image.Width; - Span red = scratch[..width]; - Span green = scratch.Slice(width, width); - Span blue = scratch.Slice(width * 2, width); - int sourceY = y + this.sourceY; - ReadOnlySpan luma = this.picture.GetRowSpan(HevcPlane.Y, sourceY).Slice(this.sourceX, width); - ConvertSamplesToFloat(luma, red); - - int packedOffset = width * 3; - if (this.picture.ChromaFormat != 0) - { - int chromaHeight = this.picture.GetHeight(HevcPlane.Cb); - GetChromaCoordinates(sourceY, this.subY, this.verticalPosition, chromaHeight - 1, out int y0, out int y1, out int y1Weight); - ReadOnlySpan cb0 = this.picture.GetRowSpan(HevcPlane.Cb, y0); - ReadOnlySpan cb1 = this.picture.GetRowSpan(HevcPlane.Cb, y1); - ReadOnlySpan cr0 = this.picture.GetRowSpan(HevcPlane.Cr, y0); - ReadOnlySpan cr1 = this.picture.GetRowSpan(HevcPlane.Cr, y1); - if (this.subX == 0) - { - ConvertSamplesToFloat(cb0.Slice(this.sourceX, width), green); - ConvertSamplesToFloat(cr0.Slice(this.sourceX, width), blue); - } - else - { - int chromaWidth = this.picture.GetWidth(HevcPlane.Cb); - Span reconstructed = scratch.Slice(packedOffset, this.picture.Width); - Span chroma0 = scratch.Slice(packedOffset + this.picture.Width, chromaWidth); - Span chroma1 = scratch.Slice(packedOffset + this.picture.Width + chromaWidth, chromaWidth); - bool isCenteredX = this.horizontalPosition == 1; - - ReconstructChromaRow( - cb0, - cb1, - y1Weight, - this.subX, - isCenteredX, - reconstructed, - chroma0, - chroma1); - - reconstructed.Slice(this.sourceX, width).CopyTo(green); - ReconstructChromaRow( - cr0, - cr1, - y1Weight, - this.subX, - isCenteredX, - reconstructed, - chroma0, - chroma1); - - reconstructed.Slice(this.sourceX, width).CopyTo(blue); - packedOffset += this.picture.Width + (chromaWidth * 2); - } - } - - this.colorConverter.ConvertToRgbInPlace(red, green, blue); - Span destination = this.image.PixelBuffer.DangerousGetRowSpan(y); - Span 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 byteStorage = MemoryMarshal.AsBytes(packedStorage)[..(width * 3)]; - Span redBytes = byteStorage[..width]; - Span greenBytes = byteStorage.Slice(width, width); - Span blueBytes = byteStorage.Slice(width * 2, width); - SimdUtils.NormalizedFloatToByteSaturate(red, redBytes); - SimdUtils.NormalizedFloatToByteSaturate(green, greenBytes); - SimdUtils.NormalizedFloatToByteSaturate(blue, blueBytes); - - PixelOperations.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, destination); - - return; - } - - Span packed = MemoryMarshal.Cast(packedStorage)[..width]; - PackRgba64(red, green, blue, packed); - PixelOperations.Instance.FromRgba64(this.configuration, packed, destination); - } - } - - /// - /// Converts packed image rows to HEVC component planes using pooled planar storage. - /// - /// The source pixel type. - private readonly struct RgbToYuvRowConverter - where TPixel : unmanaged, IPixel - { - /// - /// The configuration used for packed-pixel conversion. - /// - private readonly Configuration configuration; - - /// - /// The destination HEVC picture. - /// - private readonly HevcPictureBuffer picture; - - /// - /// The source image frame. - /// - private readonly ImageFrame image; - - /// - /// The selected H.273 color converter. - /// - private readonly HeifColorConverterBase colorConverter; - - /// - /// The largest encoded luma sample value. - /// - private readonly float lumaMaximum; - - /// - /// The largest encoded chroma sample value. - /// - private readonly float chromaMaximum; - - /// - /// The horizontal chroma offset in half-luma-sample units. - /// - private readonly int horizontalPosition; - - /// - /// The vertical chroma offset in half-luma-sample units. - /// - private readonly int verticalPosition; - - /// - /// The horizontal chroma subsampling shift. - /// - private readonly int subX; - - /// - /// The vertical chroma subsampling shift. - /// - private readonly int subY; - - /// - /// Initializes a new instance of the struct. - /// - /// The configuration used for pixel conversion. - /// The destination HEVC picture. - /// The source image frame. - /// The selected H.273 color converter. - /// The progressive-frame 4:2:0 chroma sample location. - /// The resolved H.273 component ranges. - public RgbToYuvRowConverter( - Configuration configuration, - HevcPictureBuffer picture, - ImageFrame 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); - } - - /// - /// Gets a value indicating whether source pixels use JPEG-compatible eight-bit RGB plane unpacking. - /// - public bool UsesByteInput => this.picture.BitDepthLuma == 8 && (this.picture.ChromaFormat == 0 || this.picture.BitDepthChroma == 8); - - /// - /// Gets the number of float elements required by the reusable component buffer. - /// - public int ComponentBufferLength => this.image.Width * (this.subY == 0 ? 3 : 6); - - /// - /// Converts every packed source row to the destination component planes. - /// - /// The reusable high-bit-depth RGB staging row. - /// The reusable planar component buffer. - public void Convert(Span packed, Span 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 luma = components.Slice(offset, width); - Span chromaBlue = components.Slice(offset + width, width); - Span chromaRed = components.Slice(offset + (width * 2), width); - this.ConvertSourceRow(y, packed, luma, chromaBlue, chromaRed); - WriteSamples( - 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.Empty, Span.Empty, 0F); - continue; - } - - bool isLastRow = y == this.image.Height - 1; - if (this.verticalPosition == 0 && (y & 1) == 0) - { - this.WriteChromaRows(y >> 1, chromaBlue, chromaRed, Span.Empty, Span.Empty, 0F); - } - else if (this.verticalPosition == 1 && ((y & 1) != 0 || isLastRow)) - { - if ((y & 1) != 0) - { - Span previousBlue = components.Slice(width, width); - Span previousRed = components.Slice(width * 2, width); - this.WriteChromaRows(y >> 1, previousBlue, previousRed, chromaBlue, chromaRed, 0.5F); - } - else - { - this.WriteChromaRows(y >> 1, chromaBlue, chromaRed, Span.Empty, Span.Empty, 0F); - } - } - else if (this.verticalPosition == 2 && ((y & 1) != 0 || isLastRow)) - { - this.WriteChromaRows(y >> 1, chromaBlue, chromaRed, Span.Empty, Span.Empty, 0F); - } - } - } - - /// - /// Converts one packed source row into normalized planar components. - /// - /// The source row index. - /// The high-bit-depth RGB staging row. - /// The destination luma or first component values. - /// The destination blue-difference or second component values. - /// The destination red-difference or third component values. - private void ConvertSourceRow(int y, Span packed, Span luma, Span chromaBlue, Span chromaRed) - { - ReadOnlySpan 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.Instance.UnpackIntoRgbPlanes(luma, chromaBlue, chromaRed, source); - this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, ByteMaximum); - return; - } - - PixelOperations.Instance.ToRgb48(this.configuration, source, packed); - DeinterleaveRgb48(packed, luma, chromaBlue, chromaRed); - this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, UShortMaximum); - } - - /// - /// Filters and writes one pair of chroma component rows. - /// - /// The destination chroma row. - /// The first blue-difference source row. - /// The first red-difference source row. - /// The optional second blue-difference source row. - /// The optional second red-difference source row. - /// The second-row contribution. - private void WriteChromaRows( - int destinationY, - ReadOnlySpan blue0, - ReadOnlySpan red0, - ReadOnlySpan blue1, - ReadOnlySpan red1, - float row1Weight) - { - Span blueDestination = this.picture.GetRowSpan(HevcPlane.Cb, destinationY); - Span redDestination = this.picture.GetRowSpan(HevcPlane.Cr, destinationY); - if (this.subX == 0) - { - WriteSamples( - blue0, - blueDestination, - this.colorConverter.ChromaScale, - this.colorConverter.ChromaBias, - this.chromaMaximum); - - WriteSamples( - red0, - redDestination, - this.colorConverter.ChromaScale, - this.colorConverter.ChromaBias, - this.chromaMaximum); - - return; - } - - bool isCenteredX = this.horizontalPosition == 1; - WriteSubsampledSamples( - blue0, - blue1, - blueDestination, - isCenteredX, - row1Weight, - this.colorConverter.ChromaScale, - this.colorConverter.ChromaBias, - this.chromaMaximum); - - WriteSubsampledSamples( - red0, - red1, - redDestination, - isCenteredX, - row1Weight, - this.colorConverter.ChromaScale, - this.colorConverter.ChromaBias, - this.chromaMaximum); - } - } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs index 54fafd1e5d..9ef743fe4b 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs @@ -3,6 +3,7 @@ using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Color; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.PixelFormats; diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs index 557bbf5c61..38ee48541d 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs @@ -3,6 +3,7 @@ using System; using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Color; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -320,6 +321,40 @@ public class Av1YuvConverterTests Assert.Equal(expectedLeftBlue, image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(1)[0].B); } + /// + /// Verifies that encoding selects the horizontal and vertical sample coordinates defined by each AV1 chroma position. + /// + /// The signaled AV1 chroma sample position. + /// The expected encoded blue-difference sample. + /// The expected encoded red-difference sample. + [Theory] + [InlineData(ObuChromoSamplePosition.Unknown, 128, 128)] + [InlineData(ObuChromoSamplePosition.Vertical, 96, 192)] + [InlineData(ObuChromoSamplePosition.Colocated, 128, 128)] + public void RgbToYuv420UsesChromaSamplePosition(int chromaSamplePosition, byte expectedChromaBlue, byte expectedChromaRed) + { + using Image source = new(2, 2); + + // The four distinct YCgCo samples make left, centered, and vertically averaged selection observable as + // exact integer chroma values without introducing transfer-function or coefficient-rounding tolerances. + 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); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 2, + 2, + matrixCoefficients: ObuMatrixCoefficients.SmpteYCgCo, + colorFormat: Av1ColorFormat.Yuv420, + chromaSamplePosition: (ObuChromoSamplePosition)chromaSamplePosition); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); + + Assert.Equal(expectedChromaBlue, frameBuffer.DeriveBlockPointer(Av1Plane.U, 1, 1).DangerousGetRowSpan(0)[0]); + Assert.Equal(expectedChromaRed, frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 1).DangerousGetRowSpan(0)[0]); + } + /// /// Verifies that odd image dimensions retain the final YUV 4:2:0 chroma row and column. ///