Browse Source

Reverse chroma sub sampling

pull/1731/head
Brian Popow 5 years ago
parent
commit
617a66d120
  1. 8
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
  2. 27
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs
  3. 53
      src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs
  4. 14
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  5. 9
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  6. 12
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  7. 4
      tests/ImageSharp.Tests/TestImages.cs
  8. 3
      tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h1v1.tiff
  9. 3
      tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v1.tiff
  10. 3
      tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h2v2.tiff
  11. 3
      tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v2.tiff
  12. 3
      tests/Images/Input/Tiff/rgb-ycbcr-contig-08_h4v4.tiff

8
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<TPixel>
{
public static TiffBaseColorDecoder<TPixel> 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<TPixel>(bitsPerSample, colorMap);
case TiffColorType.YCbCr:
return new YCbCrTiffColor<TPixel>(referenceBlackAndWhite, ycbcrCoefficients);
return new YCbCrTiffColor<TPixel>(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<TPixel>(bitsPerSample);
case TiffColorType.YCbCrPlanar:
return new YCbCrPlanarTiffColor<TPixel>(referenceBlackAndWhite, ycbcrCoefficients);
return new YCbCrPlanarTiffColor<TPixel>(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling);
default:
throw TiffThrowHelper.InvalidColorType(colorType.ToString());

27
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;
}
/// <inheritdoc/>
public override void Decode(IMemoryOwner<byte>[] data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
@ -22,6 +28,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation
Span<byte> cbData = data[1].GetSpan();
Span<byte> 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<byte> planarCb, Span<byte> 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];
}
}
}
}
}

53
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<TPixel> : TiffBaseColorDecoder<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
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;
}
/// <inheritdoc/>
public override void Decode(ReadOnlySpan<byte> data, Buffer2D<TPixel> pixels, int left, int top, int width, int height)
{
ReadOnlySpan<byte> ycbcrData = data;
if (this.ycbcrSubSampling != null && !(this.ycbcrSubSampling[0] == 1 && this.ycbcrSubSampling[1] == 1))
{
using IMemoryOwner<byte> tmpBuffer = this.memoryAllocator.Allocate<byte>(data.Length);
Span<byte> 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<TPixel> 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<byte> source, Span<byte> 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<byte> 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];
}
}
}
}
}
}
}

14
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -75,10 +75,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary>
public TiffColorType ColorType { get; set; }
/// <summary>
/// Gets or sets the reference black and white for decoding YCbCr pixel data.
/// </summary>
public Rational[] ReferenceBlackAndWhite { get; set; }
/// <summary>
/// Gets or sets the YCbCr coefficients.
/// </summary>
public Rational[] YcbcrCoefficients { get; set; }
/// <summary>
/// Gets or sets the YCbCr sub sampling.
/// </summary>
public ushort[] YcbcrSubSampling { get; set; }
/// <summary>
/// Gets or sets the compression used, when the image was encoded.
/// </summary>
@ -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<TPixel> colorDecoder = TiffColorDecoderFactory<TPixel>.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++)

9
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);

12
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<TPixel> image = provider.GetImage();
image.DebugSave(provider);
}
[Theory]
[WithFile(FlowerRgb101010Contiguous, PixelTypes.Rgba32)]

4
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";

3
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

3
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

3
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

3
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

3
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
Loading…
Cancel
Save