From 617a66d120c9d4e74611b7c05b534738fa210def Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 9 Aug 2021 11:32:10 +0200 Subject: [PATCH] Reverse chroma sub sampling --- .../TiffColorDecoderFactory{TPixel}.cs | 8 ++- .../YCbCrPlanarTiffColor{TPixel}.cs | 27 +++++++++- .../YCbCrTiffColor{TPixel}.cs | 53 ++++++++++++++++++- .../Formats/Tiff/TiffDecoderCore.cs | 14 +++++ .../Formats/Tiff/TiffDecoderOptionsParser.cs | 9 ++-- .../Formats/Tiff/TiffDecoderTests.cs | 12 ++++- tests/ImageSharp.Tests/TestImages.cs | 4 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff | 3 ++ .../Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff | 3 ++ 12 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff create mode 100644 tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index d27971274..13f95fedf 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation @@ -9,11 +10,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation where TPixel : unmanaged, IPixel { public static TiffBaseColorDecoder Create( + MemoryAllocator memoryAllocator, TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, Rational[] referenceBlackAndWhite, Rational[] ycbcrCoefficients, + ushort[] ycbcrSubSampling, ByteOrder byteOrder) { switch (colorType) @@ -147,7 +150,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new PaletteTiffColor(bitsPerSample, colorMap); case TiffColorType.YCbCr: - return new YCbCrTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + return new YCbCrTiffColor(memoryAllocator, referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); @@ -160,6 +163,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ushort[] colorMap, Rational[] referenceBlackAndWhite, Rational[] ycbcrCoefficients, + ushort[] ycbcrSubSampling, ByteOrder byteOrder) { switch (colorType) @@ -174,7 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new RgbPlanarTiffColor(bitsPerSample); case TiffColorType.YCbCrPlanar: - return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs index 126033e31..0e411c69d 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -13,7 +13,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation { private readonly YCbCrConverter converter; - public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + private readonly ushort[] ycbcrSubSampling; + + public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients, ushort[] ycbcrSubSampling) + { + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + this.ycbcrSubSampling = ycbcrSubSampling; + } /// public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) @@ -22,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Span cbData = data[1].GetSpan(); Span crData = data[2].GetSpan(); + if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1)) + { + ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], cbData, crData); + } + var color = default(TPixel); int offset = 0; for (int y = top; y < top + height; y++) @@ -36,5 +47,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } } + + private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, Span planarCb, Span planarCr) + { + for (int row = height - 1; row >= 0; row--) + { + for (int col = width - 1; col >= 0; col--) + { + int offset = (row * width) + col; + int subSampleOffset = (row / verticalSubSampling * (width / horizontalSubSampling)) + (col / horizontalSubSampling); + planarCb[offset] = planarCb[subSampleOffset]; + planarCr[offset] = planarCr[subSampleOffset]; + } + } + } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index 067827212..7a7e1ad87 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -10,13 +11,31 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal class YCbCrTiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { + private readonly MemoryAllocator memoryAllocator; + private readonly YCbCrConverter converter; - public YCbCrTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + private readonly ushort[] ycbcrSubSampling; + + public YCbCrTiffColor(MemoryAllocator memoryAllocator, Rational[] referenceBlackAndWhite, Rational[] coefficients, ushort[] ycbcrSubSampling) + { + this.memoryAllocator = memoryAllocator; + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + this.ycbcrSubSampling = ycbcrSubSampling; + } /// public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) { + ReadOnlySpan ycbcrData = data; + if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1)) + { + using IMemoryOwner tmpBuffer = this.memoryAllocator.Allocate(data.Length); + Span tmpBufferSpan = tmpBuffer.GetSpan(); + ReverseChromaSubSampling(width, height, this.ycbcrSubSampling[0], this.ycbcrSubSampling[1], data, tmpBufferSpan); + ycbcrData = tmpBufferSpan; + } + var color = default(TPixel); int offset = 0; for (int y = top; y < top + height; y++) @@ -24,12 +43,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { - Rgba32 rgba = this.converter.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); + Rgba32 rgba = this.converter.ConvertToRgba32(ycbcrData[offset], ycbcrData[offset + 1], ycbcrData[offset + 2]); color.FromRgba32(rgba); pixelRow[x] = color; offset += 3; } } } + + private static void ReverseChromaSubSampling(int width, int height, int horizontalSubSampling, int verticalSubSampling, ReadOnlySpan source, Span destination) + { + int blockWidth = width / horizontalSubSampling; + int blockHeight = height / verticalSubSampling; + int cbCrOffsetInBlock = horizontalSubSampling * verticalSubSampling; + int blockByteCount = cbCrOffsetInBlock + 2; + + for (int blockRow = blockHeight - 1; blockRow >= 0; blockRow--) + { + for (int blockCol = blockWidth - 1; blockCol >= 0; blockCol--) + { + int blockOffset = (blockRow * blockWidth) + blockCol; + ReadOnlySpan blockData = source.Slice(blockOffset * blockByteCount, blockByteCount); + byte cr = blockData[cbCrOffsetInBlock + 1]; + byte cb = blockData[cbCrOffsetInBlock]; + + for (int row = verticalSubSampling - 1; row >= 0; row--) + { + for (int col = horizontalSubSampling - 1; col >= 0; col--) + { + int offset = 3 * ((((blockRow * verticalSubSampling) + row) * width) + (blockCol * horizontalSubSampling) + col); + destination[offset + 2] = cr; + destination[offset + 1] = cb; + destination[offset] = blockData[(row * horizontalSubSampling) + col]; + } + } + } + } + } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 69b6c6615..080cdb22a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -75,10 +75,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffColorType ColorType { get; set; } + /// + /// Gets or sets the reference black and white for decoding YCbCr pixel data. + /// public Rational[] ReferenceBlackAndWhite { get; set; } + /// + /// Gets or sets the YCbCr coefficients. + /// public Rational[] YcbcrCoefficients { get; set; } + /// + /// Gets or sets the YCbCr sub sampling. + /// + public ushort[] YcbcrSubSampling { get; set; } + /// /// Gets or sets the compression used, when the image was encoded. /// @@ -283,6 +294,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.ColorMap, this.ReferenceBlackAndWhite, this.YcbcrCoefficients, + this.YcbcrSubSampling, this.byteOrder); for (int i = 0; i < stripsPerPlane; i++) @@ -334,11 +346,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.FaxCompressionOptions); TiffBaseColorDecoder colorDecoder = TiffColorDecoderFactory.Create( + this.memoryAllocator, this.ColorType, this.BitsPerSample, this.ColorMap, this.ReferenceBlackAndWhite, this.YcbcrCoefficients, + this.YcbcrSubSampling, this.byteOrder); for (int stripIndex = 0; stripIndex < stripOffsets.Length; stripIndex++) diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 86dc3c1df..15764cffa 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -58,14 +58,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff } ushort[] ycbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; - if (ycbcrSubSampling != null && ycbcrSubSampling[0] != ycbcrSubSampling[1]) + if (ycbcrSubSampling != null && ycbcrSubSampling.Length != 2) { - TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images with equal luma and chroma samples."); + TiffThrowHelper.ThrowImageFormatException("Invalid YCbCrSubsampling, expected 2 values."); } - if (ycbcrSubSampling != null && ycbcrSubSampling[0] != 1) + if (ycbcrSubSampling != null && ycbcrSubSampling[1] > ycbcrSubSampling[0]) { - TiffThrowHelper.ThrowNotSupported("ImageSharp only supports YCbCr images without subsampling."); + TiffThrowHelper.ThrowImageFormatException("ChromaSubsampleVert shall always be less than or equal to ChromaSubsampleHoriz."); } if (exifProfile.GetValue(ExifTag.StripRowCounts)?.Value != null) @@ -82,6 +82,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff options.BitsPerSample = frameMetadata.BitsPerSample ?? new TiffBitsPerSample(0, 0, 0); options.ReferenceBlackAndWhite = exifProfile.GetValue(ExifTag.ReferenceBlackWhite)?.Value; options.YcbcrCoefficients = exifProfile.GetValue(ExifTag.YCbCrCoefficients)?.Value; + options.YcbcrSubSampling = exifProfile.GetValue(ExifTag.YCbCrSubsampling)?.Value; options.ParseColorType(exifProfile); options.ParseCompression(frameMetadata.Compression, exifProfile); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index b2e2e2535..18007d1c4 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -161,8 +161,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerYCbCr888Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerYCbCr888Planar, PixelTypes.Rgba32)] + [WithFile(RgbYCbCr888Contiguoush1v1, PixelTypes.Rgba32)] + [WithFile(RgbYCbCr888Contiguoush2v1, PixelTypes.Rgba32)] + [WithFile(RgbYCbCr888Contiguoush2v2, PixelTypes.Rgba32)] + [WithFile(RgbYCbCr888Contiguoush4v4, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_YCbCr_24Bit(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + where TPixel : unmanaged, IPixel + { + // Note: The image from MagickReferenceDecoder does not look right, maybe we are doing something wrong + // converting the pixel data from Magick.Net to our format with YCbCr? + using Image image = provider.GetImage(); + image.DebugSave(provider); + } [Theory] [WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a79ff9da8..3ad30b545 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -577,6 +577,10 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb888Planar15Strips = "Tiff/flower-rgb-planar-08-15strips.tiff"; public const string FlowerYCbCr888Contiguous = "Tiff/flower-ycbcr-contig-08.tiff"; public const string FlowerYCbCr888Planar = "Tiff/flower-ycbcr-planar-08.tiff"; + public const string RgbYCbCr888Contiguoush1v1 = "Tiff/rgb-ycbcr-contig-08_h1v1.tiff"; + public const string RgbYCbCr888Contiguoush2v1 = "Tiff/rgb-ycbcr-contig-08_h2v1.tiff"; + public const string RgbYCbCr888Contiguoush2v2 = "Tiff/rgb-ycbcr-contig-08_h2v2.tiff"; + public const string RgbYCbCr888Contiguoush4v4 = "Tiff/rgb-ycbcr-contig-08_h4v4.tiff"; public const string FlowerRgb444Contiguous = "Tiff/flower-rgb-contig-04.tiff"; public const string FlowerRgb444Planar = "Tiff/flower-rgb-planar-04.tiff"; public const string FlowerRgb222Contiguous = "Tiff/flower-rgb-contig-02.tiff"; diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff new file mode 100644 index 000000000..801cab574 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:389ee18596cd3d9f1f7f04b4db8fd21edce2900837c17ebb57cc4b64a6820f3e +size 120354 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff new file mode 100644 index 000000000..82350e5b2 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95b1ba4ff48ea2263041eca4ada44d009277297bb3b3a185d48580bdf3f7caaf +size 81382 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff new file mode 100644 index 000000000..b282b1742 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbd835c2406700523b239b80299b2b02c36d41182ac338f7ed7164979a787c60 +size 63438 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff new file mode 100644 index 000000000..0c8c048dc --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:399d5bc062baa00c2054a138489709379032f8683fbcb292bb2125b62e715b5f +size 50336 diff --git a/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff new file mode 100644 index 000000000..45341ed26 --- /dev/null +++ b/tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58c4914b32b27df1ef303bb127fe9211c2aeda23e17bb5f4b349543c96d845b7 +size 45152