From da8f14d97f1572916c64343bea798e178e0e0fac Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 8 Aug 2021 17:29:38 +0200 Subject: [PATCH] Add support for decoding ycbcr tiff's with planar configuration --- .../TiffColorDecoderFactory{TPixel}.cs | 11 +- .../TiffColorType.cs | 10 +- .../YCbCrConverter.cs | 121 ++++++++++++++++++ .../YCbCrPlanarTiffColor{TPixel}.cs | 40 ++++++ .../YCbCrTiffColor{TPixel}.cs | 79 +----------- .../Formats/Tiff/TiffDecoderCore.cs | 19 ++- .../Formats/Tiff/TiffDecoderOptionsParser.cs | 3 +- tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Tiff/flower-ycbcr-planar-08.tiff | 3 + 9 files changed, 205 insertions(+), 82 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs create mode 100644 src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs create mode 100644 tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index 010897c3c1..d279712746 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -154,7 +154,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation } } - public static TiffBasePlanarColorDecoder CreatePlanar(TiffColorType colorType, TiffBitsPerSample bitsPerSample, ushort[] colorMap, ByteOrder byteOrder) + public static TiffBasePlanarColorDecoder CreatePlanar( + TiffColorType colorType, + TiffBitsPerSample bitsPerSample, + ushort[] colorMap, + Rational[] referenceBlackAndWhite, + Rational[] ycbcrCoefficients, + ByteOrder byteOrder) { switch (colorType) { @@ -167,6 +173,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation return new RgbPlanarTiffColor(bitsPerSample); + case TiffColorType.YCbCrPlanar: + return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients); + default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 4e108f58e3..51c9be83f0 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -108,6 +108,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// RgbPlanar, - YCbCr + /// + /// The pixels are stored in YCbCr format. + /// + YCbCr, + + /// + /// The pixels are stored in YCbCr format as planar. + /// + YCbCrPlanar } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs new file mode 100644 index 0000000000..3e28e30bc4 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrConverter.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Converts YCbCr data to rgb data. + /// + internal class YCbCrConverter + { + private readonly CodingRangeExpander yExpander; + private readonly CodingRangeExpander cbExpander; + private readonly CodingRangeExpander crExpander; + private readonly YCbCrToRgbConverter converter; + + private static readonly Rational[] DefaultLuma = + { + new Rational(299, 1000), + new Rational(587, 1000), + new Rational(114, 1000) + }; + + private static readonly Rational[] DefaultReferenceBlackWhite = + { + new Rational(0, 1), new Rational(255, 1), + new Rational(128, 1), new Rational(255, 1), + new Rational(128, 1), new Rational(255, 1) + }; + + public YCbCrConverter(Rational[] referenceBlackAndWhite, Rational[] coefficients) + { + referenceBlackAndWhite ??= DefaultReferenceBlackWhite; + coefficients ??= DefaultLuma; + + if (referenceBlackAndWhite.Length != 6) + { + TiffThrowHelper.ThrowImageFormatException("reference black and white array should have 6 entry's"); + } + + if (coefficients.Length != 3) + { + TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); + } + + this.yExpander = new CodingRangeExpander(referenceBlackAndWhite[0], referenceBlackAndWhite[1], 255); + this.cbExpander = new CodingRangeExpander(referenceBlackAndWhite[2], referenceBlackAndWhite[3], 127); + this.crExpander = new CodingRangeExpander(referenceBlackAndWhite[4], referenceBlackAndWhite[5], 127); + this.converter = new YCbCrToRgbConverter(coefficients[0], coefficients[1], coefficients[2]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 ConvertToRgba32(byte y, byte cb, byte cr) + { + float yExpanded = this.yExpander.Expand(y); + float cbExpanded = this.cbExpander.Expand(cb); + float crExpanded = this.crExpander.Expand(cr); + + Rgba32 rgba = this.converter.Convert(yExpanded, cbExpanded, crExpanded); + + return rgba; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte RoundAndClampTo8Bit(float value) + { + int input = (int)MathF.Round(value); + return (byte)Math.Min(Math.Max(input, 0), 255); + } + + private readonly struct CodingRangeExpander + { + private readonly float f1; + private readonly float f2; + + public CodingRangeExpander(Rational referenceBlack, Rational referenceWhite, int codingRange) + { + float black = referenceBlack.ToSingle(); + float white = referenceWhite.ToSingle(); + this.f1 = codingRange / (white - black); + this.f2 = this.f1 * black; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Expand(float code) => (code * this.f1) - this.f2; + } + + private readonly struct YCbCrToRgbConverter + { + private readonly float cr2R; + private readonly float cb2B; + private readonly float y2G; + private readonly float cr2G; + private readonly float cb2G; + + public YCbCrToRgbConverter(Rational lumaRed, Rational lumaGreen, Rational lumaBlue) + { + this.cr2R = 2 - (2 * lumaRed.ToSingle()); + this.cb2B = 2 - (2 * lumaBlue.ToSingle()); + this.y2G = (1 - lumaBlue.ToSingle() - lumaRed.ToSingle()) / lumaGreen.ToSingle(); + this.cr2G = 2 * lumaRed.ToSingle() * (lumaRed.ToSingle() - 1) / lumaGreen.ToSingle(); + this.cb2G = 2 * lumaBlue.ToSingle() * (lumaBlue.ToSingle() - 1) / lumaGreen.ToSingle(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Rgba32 Convert(float y, float cb, float cr) + { + var pixel = default(Rgba32); + pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y); + pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb)); + pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y); + pixel.A = byte.MaxValue; + + return pixel; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs new file mode 100644 index 0000000000..126033e318 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + internal class YCbCrPlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly YCbCrConverter converter; + + public YCbCrPlanarTiffColor(Rational[] referenceBlackAndWhite, Rational[] coefficients) => this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + Span yData = data[0].GetSpan(); + Span cbData = data[1].GetSpan(); + Span crData = data[2].GetSpan(); + + var color = default(TPixel); + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + Rgba32 rgba = this.converter.ConvertToRgba32(yData[offset], cbData[offset], crData[offset]); + color.FromRgba32(rgba); + pixelRow[x] = color; + offset++; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index 18a85f4e81..55ad67a3cf 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -11,10 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation internal class YCbCrTiffColor : TiffBaseColorDecoder where TPixel : unmanaged, IPixel { - private readonly CodingRangeExpander yExpander; - private readonly CodingRangeExpander cbExpander; - private readonly CodingRangeExpander crExpander; - private readonly YCbCrToRgbConverter converter; + private readonly YCbCrConverter converter; private static readonly Rational[] DefaultLuma = { @@ -45,10 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation TiffThrowHelper.ThrowImageFormatException("luma coefficients array should have 6 entry's"); } - this.yExpander = new CodingRangeExpander(referenceBlackAndWhite[0], referenceBlackAndWhite[1], 255); - this.cbExpander = new CodingRangeExpander(referenceBlackAndWhite[2], referenceBlackAndWhite[3], 127); - this.crExpander = new CodingRangeExpander(referenceBlackAndWhite[4], referenceBlackAndWhite[5], 127); - this.converter = new YCbCrToRgbConverter(coefficients[0], coefficients[1], coefficients[2]); + this.converter = new YCbCrConverter(referenceBlackAndWhite, coefficients); } /// @@ -61,78 +54,12 @@ 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.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); + Rgba32 rgba = this.converter.ConvertToRgba32(data[offset], data[offset + 1], data[offset + 2]); color.FromRgba32(rgba); pixelRow[x] = color; offset += 3; } } } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Rgba32 ConvertToRgba32(byte y, byte cb, byte cr) - { - float yExpanded = this.yExpander.Expand(y); - float cbExpanded = this.cbExpander.Expand(cb); - float crExpanded = this.crExpander.Expand(cr); - - Rgba32 rgba = this.converter.Convert(yExpanded, cbExpanded, crExpanded); - - return rgba; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static byte RoundAndClampTo8Bit(float value) - { - int input = (int)MathF.Round(value); - return (byte)Math.Min(Math.Max(input, 0), 255); - } - - private readonly struct CodingRangeExpander - { - private readonly float f1; - private readonly float f2; - - public CodingRangeExpander(Rational referenceBlack, Rational referenceWhite, int codingRange) - { - float black = referenceBlack.ToSingle(); - float white = referenceWhite.ToSingle(); - this.f1 = codingRange / (white - black); - this.f2 = this.f1 * black; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float Expand(float code) => (code * this.f1) - this.f2; - } - - private readonly struct YCbCrToRgbConverter - { - private readonly float cr2R; - private readonly float cb2B; - private readonly float y2G; - private readonly float cr2G; - private readonly float cb2G; - - public YCbCrToRgbConverter(Rational lumaRed, Rational lumaGreen, Rational lumaBlue) - { - this.cr2R = 2 - (2 * lumaRed.ToSingle()); - this.cb2B = 2 - (2 * lumaBlue.ToSingle()); - this.y2G = (1 - lumaBlue.ToSingle() - lumaRed.ToSingle()) / lumaGreen.ToSingle(); - this.cr2G = 2 * lumaRed.ToSingle() * (lumaRed.ToSingle() - 1) / lumaGreen.ToSingle(); - this.cb2G = 2 * lumaBlue.ToSingle() * (lumaBlue.ToSingle() - 1) / lumaGreen.ToSingle(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Rgba32 Convert(float y, float cb, float cr) - { - var pixel = default(Rgba32); - pixel.R = RoundAndClampTo8Bit((cr * this.cr2R) + y); - pixel.G = RoundAndClampTo8Bit((this.y2G * y) + (this.cr2G * cr) + (this.cb2G * cb)); - pixel.B = RoundAndClampTo8Bit((cb * this.cb2B) + y); - pixel.A = byte.MaxValue; - - return pixel; - } - } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 1693ed4528..69b6c66157 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -268,9 +268,22 @@ namespace SixLabors.ImageSharp.Formats.Tiff stripBuffers[stripIndex] = this.memoryAllocator.Allocate(uncompressedStripSize); } - using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(this.CompressionType, this.memoryAllocator, this.PhotometricInterpretation, frame.Width, bitsPerPixel, this.Predictor, this.FaxCompressionOptions); - - TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar(this.ColorType, this.BitsPerSample, this.ColorMap, this.byteOrder); + using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( + this.CompressionType, + this.memoryAllocator, + this.PhotometricInterpretation, + frame.Width, + bitsPerPixel, + this.Predictor, + this.FaxCompressionOptions); + + TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar( + this.ColorType, + this.BitsPerSample, + this.ColorMap, + this.ReferenceBlackAndWhite, + this.YcbcrCoefficients, + this.byteOrder); for (int i = 0; i < stripsPerPlane; i++) { diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index 8ab543b500..857509f87a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -274,7 +274,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); } - options.ColorType = TiffColorType.YCbCr; + options.ColorType = options.PlanarConfiguration == TiffPlanarConfiguration.Chunky ? TiffColorType.YCbCr : TiffColorType.YCbCrPlanar; + break; } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index ce3e7ecb3c..5c2d3ceb55 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -576,6 +576,7 @@ namespace SixLabors.ImageSharp.Tests public const string FlowerRgb888Planar6Strips = "Tiff/flower-rgb-planar-08-6strips.tiff"; 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 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/flower-ycbcr-planar-08.tiff b/tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff new file mode 100644 index 0000000000..4506ff3e93 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-ycbcr-planar-08.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdc4d8033214a6737f41c4e32d9314db77b3b1ae14515496f10468047390f6c5 +size 10042