trisquel-icecat/icecat/gfx/tests/gtest/TestYUVtoRGB32.cpp

510 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "gtest/gtest.h"
#include <array>
#include <cmath>
#include <tuple>
#include <unordered_map>
#include "ImageContainer.h"
#include "YCbCrUtils.h"
#include "nsTArray.h"
using Color = std::tuple<uint8_t, uint8_t, uint8_t>;
using namespace mozilla;
const Color BLACK(0, 0, 0);
const Color BLUE(0, 0, 255);
const Color GREEN(0, 255, 0);
const Color CYAN(0, 255, 255);
const Color RED(255, 0, 0);
const Color MAGENTA(255, 0, 255);
const Color YELLOW(255, 255, 0);
const Color WHITE(255, 255, 255);
const Color CHOCOLATE(210, 105, 30);
const Color PERU(205, 133, 63);
const Color ROSYBROWN(188, 143, 143);
const Color STEELBLUE(70, 130, 180);
const std::array<Color, 12> COLOR_LIST = {
BLACK, BLUE, GREEN, CYAN, RED, MAGENTA,
YELLOW, WHITE, CHOCOLATE, PERU, ROSYBROWN, STEELBLUE};
Color RGB2YUV(const Color& aRGBColor) {
const uint8_t& r = std::get<0>(aRGBColor);
const uint8_t& g = std::get<1>(aRGBColor);
const uint8_t& b = std::get<2>(aRGBColor);
const double y = r * 0.299 + g * 0.587 + b * 0.114;
const double u = r * -0.168736 + g * -0.331264 + b * 0.5 + 128;
const double v = r * 0.5 + g * -0.418688 + b * -0.081312 + 128;
return Color(round(y), round(u), round(v));
}
int32_t CeilingOfHalf(int32_t aValue) {
MOZ_ASSERT(aValue >= 0);
return aValue / 2 + (aValue % 2);
}
already_AddRefed<layers::PlanarYCbCrImage> CreateI420Image(
const Color& aRGBColor, const gfx::YUVColorSpace& aColorSpace,
const gfx::IntSize& aSize, Maybe<uint8_t> aAlphaValue = Nothing()) {
const int32_t halfWidth = CeilingOfHalf(aSize.width);
const int32_t halfHeight = CeilingOfHalf(aSize.height);
const size_t yPlaneSize = aSize.width * aSize.height;
const size_t uPlaneSize = halfWidth * halfHeight;
const size_t vPlaneSize = uPlaneSize;
const size_t aPlaneSize = aAlphaValue.isSome() ? yPlaneSize : 0;
const size_t imageSize = yPlaneSize + uPlaneSize + vPlaneSize + aPlaneSize;
const Color yuvColor = RGB2YUV(aRGBColor);
const uint8_t& yColor = std::get<0>(yuvColor);
const uint8_t& uColor = std::get<1>(yuvColor);
const uint8_t& vColor = std::get<2>(yuvColor);
UniquePtr<uint8_t[]> buffer(new uint8_t[imageSize]);
layers::PlanarYCbCrData data;
data.mPictureRect = gfx::IntRect({0, 0}, aSize);
// Y plane.
uint8_t* yChannel = buffer.get();
memset(yChannel, yColor, yPlaneSize);
data.mYChannel = yChannel;
data.mYStride = aSize.width;
data.mYSkip = 0;
// Cb plane (aka U).
uint8_t* uChannel = yChannel + yPlaneSize;
memset(uChannel, uColor, uPlaneSize);
data.mCbChannel = uChannel;
data.mCbSkip = 0;
// Cr plane (aka V).
uint8_t* vChannel = uChannel + uPlaneSize;
memset(vChannel, vColor, vPlaneSize);
data.mCrChannel = vChannel;
data.mCrSkip = 0;
// CrCb plane vectors.
data.mCbCrStride = halfWidth;
data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
// Alpha plane.
if (aPlaneSize) {
uint8_t* aChannel = vChannel + vPlaneSize;
memset(aChannel, *aAlphaValue, aPlaneSize);
data.mAlpha.emplace();
data.mAlpha->mChannel = aChannel;
data.mAlpha->mSize = aSize;
}
data.mYUVColorSpace = aColorSpace;
RefPtr<layers::PlanarYCbCrImage> image =
new layers::RecyclingPlanarYCbCrImage(new layers::BufferRecycleBin());
image->CopyData(data);
return image.forget();
}
already_AddRefed<layers::PlanarYCbCrImage> CreateI444Image(
const Color& aRGBColor, const gfx::YUVColorSpace& aColorSpace,
const gfx::IntSize& aSize, Maybe<uint8_t> aAlphaValue = Nothing()) {
const size_t yPlaneSize = aSize.width * aSize.height;
const size_t uPlaneSize = yPlaneSize;
const size_t vPlaneSize = yPlaneSize;
const size_t aPlaneSize = aAlphaValue.isSome() ? yPlaneSize : 0;
const size_t imageSize = yPlaneSize + uPlaneSize + vPlaneSize + aPlaneSize;
const Color yuvColor = RGB2YUV(aRGBColor);
const uint8_t& yColor = std::get<0>(yuvColor);
const uint8_t& uColor = std::get<1>(yuvColor);
const uint8_t& vColor = std::get<2>(yuvColor);
UniquePtr<uint8_t[]> buffer(new uint8_t[imageSize]);
layers::PlanarYCbCrData data;
data.mPictureRect = gfx::IntRect({0, 0}, aSize);
// Y plane.
uint8_t* yChannel = buffer.get();
memset(yChannel, yColor, yPlaneSize);
data.mYChannel = yChannel;
data.mYStride = aSize.width;
data.mYSkip = 0;
// Cb plane (aka U).
uint8_t* uChannel = yChannel + yPlaneSize;
memset(uChannel, uColor, uPlaneSize);
data.mCbChannel = uChannel;
data.mCbSkip = 0;
// Cr plane (aka V).
uint8_t* vChannel = uChannel + uPlaneSize;
memset(vChannel, vColor, vPlaneSize);
data.mCrChannel = vChannel;
data.mCrSkip = 0;
// CrCb plane vectors.
data.mCbCrStride = data.mYStride;
data.mChromaSubsampling = gfx::ChromaSubsampling::FULL;
// Alpha plane.
if (aPlaneSize) {
uint8_t* aChannel = vChannel + vPlaneSize;
memset(aChannel, *aAlphaValue, aPlaneSize);
data.mAlpha.emplace();
data.mAlpha->mChannel = aChannel;
data.mAlpha->mSize = aSize;
}
data.mYUVColorSpace = aColorSpace;
RefPtr<layers::PlanarYCbCrImage> image =
new layers::RecyclingPlanarYCbCrImage(new layers::BufferRecycleBin());
image->CopyData(data);
return image.forget();
}
void IsColorEqual(uint8_t* aBGRX, uint8_t* aRGBX, size_t aSize) {
ASSERT_EQ(aSize % 4, (size_t)0);
for (size_t i = 0; i < aSize; i += 4) {
ASSERT_EQ(aBGRX[i + 2], aRGBX[i]); // R
ASSERT_EQ(aBGRX[i + 1], aRGBX[i + 1]); // G
ASSERT_EQ(aBGRX[i], aRGBX[i + 2]); // B
ASSERT_EQ(aBGRX[i + 3], aRGBX[i + 3]); // X or A
}
}
uint32_t Hash(const Color& aColor) {
const uint8_t& r = std::get<0>(aColor);
const uint8_t& g = std::get<1>(aColor);
const uint8_t& b = std::get<2>(aColor);
return r << 16 | g << 8 | b;
}
std::unordered_map<uint32_t, std::array<Color, 3>> GetExpectedConvertedRGB() {
static std::unordered_map<uint32_t, std::array<Color, 3>> map;
map.emplace(Hash(BLACK), std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(0, 0, 0),
// gfx::YUVColorSpace::BT709
Color(0, 0, 0),
// gfx::YUVColorSpace::BT2020
Color(0, 0, 0)});
map.emplace(Hash(BLUE), std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(0, 82, 0),
// gfx::YUVColorSpace::BT709
Color(0, 54, 0),
// gfx::YUVColorSpace::BT2020
Color(0, 53, 0)});
map.emplace(Hash(GREEN), std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(0, 255, 0),
// gfx::YUVColorSpace::BT709
Color(0, 231, 0),
// gfx::YUVColorSpace::BT2020
Color(0, 242, 0)});
map.emplace(Hash(CYAN), std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(0, 255, 255),
// gfx::YUVColorSpace::BT709
Color(0, 248, 255),
// gfx::YUVColorSpace::BT2020
Color(0, 255, 255)});
map.emplace(Hash(RED), std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(0, 191, 0),
// gfx::YUVColorSpace::BT709
Color(0, 147, 0),
// gfx::YUVColorSpace::BT2020
Color(0, 162, 0)});
map.emplace(Hash(MAGENTA), std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(255, 0, 255),
// gfx::YUVColorSpace::BT709
Color(255, 28, 255),
// gfx::YUVColorSpace::BT2020
Color(255, 18, 255)});
map.emplace(Hash(YELLOW), std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(255, 255, 0),
// gfx::YUVColorSpace::BT709
Color(255, 255, 0),
// gfx::YUVColorSpace::BT2020
Color(255, 255, 0)});
map.emplace(Hash(WHITE), std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(255, 255, 255),
// gfx::YUVColorSpace::BT709
Color(255, 255, 255),
// gfx::YUVColorSpace::BT2020
Color(255, 255, 255)});
map.emplace(Hash(CHOCOLATE),
std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(224, 104, 20),
// gfx::YUVColorSpace::BT709
Color(236, 111, 20),
// gfx::YUVColorSpace::BT2020
Color(229, 102, 20)});
map.emplace(Hash(PERU), std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(219, 137, 58),
// gfx::YUVColorSpace::BT709
Color(228, 140, 58),
// gfx::YUVColorSpace::BT2020
Color(223, 134, 59)});
map.emplace(Hash(ROSYBROWN),
std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(200, 147, 147),
// gfx::YUVColorSpace::BT709
Color(204, 152, 147),
// gfx::YUVColorSpace::BT2020
Color(201, 149, 147)});
map.emplace(Hash(STEELBLUE),
std::array<Color, 3>{// gfx::YUVColorSpace::BT601
Color(65, 133, 189),
// gfx::YUVColorSpace::BT709
Color(58, 129, 189),
// gfx::YUVColorSpace::BT2020
Color(62, 135, 189)});
return map;
}
void IsColorMatched(const Color& aColor, uint8_t* aRGBX, size_t aSize,
Maybe<uint8_t> aAlphaValue = Nothing()) {
const uint8_t& r = std::get<0>(aColor);
const uint8_t& g = std::get<1>(aColor);
const uint8_t& b = std::get<2>(aColor);
for (size_t i = 0; i < aSize; i += 4) {
ASSERT_EQ(r, aRGBX[i]); // R
ASSERT_EQ(g, aRGBX[i + 1]); // G
ASSERT_EQ(b, aRGBX[i + 2]); // B
if (aAlphaValue) {
ASSERT_EQ(*aAlphaValue, aRGBX[i + 3]); // A
}
}
}
TEST(YCbCrUtils, ConvertYCbCrToRGB32)
{
const gfx::IntSize imgSize(32, 16);
const int32_t stride =
imgSize.Width() * gfx::BytesPerPixel(gfx::SurfaceFormat::B8G8R8X8);
const size_t bufferSize = stride * imgSize.Height();
const std::array<gfx::YUVColorSpace, 3> colorSpaces{
gfx::YUVColorSpace::BT601, gfx::YUVColorSpace::BT709,
gfx::YUVColorSpace::BT2020};
std::unordered_map<uint32_t, std::array<Color, 3>> expectations =
GetExpectedConvertedRGB();
for (const Color& color : COLOR_LIST) {
const std::array<Color, 3>& expectedColors = expectations[Hash(color)];
for (const gfx::YUVColorSpace& colorSpace : colorSpaces) {
RefPtr<layers::PlanarYCbCrImage> img =
CreateI420Image(color, colorSpace, imgSize);
UniquePtr<uint8_t[]> BGRX = MakeUnique<uint8_t[]>(bufferSize);
ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::B8G8R8X8,
BGRX.get(), stride, nullptr);
UniquePtr<uint8_t[]> RGBX = MakeUnique<uint8_t[]>(bufferSize);
ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::R8G8B8X8,
RGBX.get(), stride, nullptr);
IsColorEqual(BGRX.get(), RGBX.get(), bufferSize);
Color expectation = expectedColors[static_cast<size_t>(colorSpace)];
IsColorMatched(expectation, RGBX.get(), bufferSize);
}
}
}
TEST(YCbCrUtils, ConvertYCbCrToRGB32WithAlpha)
{
const gfx::IntSize imgSize(32, 16);
const int32_t stride =
imgSize.Width() * gfx::BytesPerPixel(gfx::SurfaceFormat::B8G8R8A8);
const size_t bufferSize = stride * imgSize.Height();
const std::array<gfx::YUVColorSpace, 3> colorSpaces{
gfx::YUVColorSpace::BT601, gfx::YUVColorSpace::BT709,
gfx::YUVColorSpace::BT2020};
std::unordered_map<uint32_t, std::array<Color, 3>> expectations =
GetExpectedConvertedRGB();
for (const Color& color : COLOR_LIST) {
const std::array<Color, 3>& expectedColors = expectations[Hash(color)];
for (const gfx::YUVColorSpace& colorSpace : colorSpaces) {
Maybe<uint8_t> alpha = Some(128);
RefPtr<layers::PlanarYCbCrImage> img =
CreateI420Image(color, colorSpace, imgSize, alpha);
UniquePtr<uint8_t[]> BGRA = MakeUnique<uint8_t[]>(bufferSize);
ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::B8G8R8A8,
BGRA.get(), stride, nullptr);
UniquePtr<uint8_t[]> RGBA = MakeUnique<uint8_t[]>(bufferSize);
ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::R8G8B8A8,
RGBA.get(), stride, nullptr);
IsColorEqual(BGRA.get(), RGBA.get(), bufferSize);
Color expectation = expectedColors[static_cast<size_t>(colorSpace)];
IsColorMatched(expectation, RGBA.get(), bufferSize, alpha);
}
}
}
TEST(YCbCrUtils, ConvertYCbCrToRGB32WithIdentityColorSpace)
{
const gfx::IntSize imgSize(32, 16);
const int32_t stride =
imgSize.Width() * gfx::BytesPerPixel(gfx::SurfaceFormat::B8G8R8X8);
const size_t bufferSize = stride * imgSize.Height();
for (const Color& color : COLOR_LIST) {
RefPtr<layers::PlanarYCbCrImage> img =
CreateI444Image(color, gfx::YUVColorSpace::Identity, imgSize);
UniquePtr<uint8_t[]> BGRX = MakeUnique<uint8_t[]>(bufferSize);
ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::B8G8R8X8,
BGRX.get(), stride, nullptr);
UniquePtr<uint8_t[]> RGBX = MakeUnique<uint8_t[]>(bufferSize);
ConvertYCbCrToRGB32(*img->GetData(), gfx::SurfaceFormat::R8G8B8X8,
RGBX.get(), stride, nullptr);
IsColorEqual(BGRX.get(), RGBX.get(), bufferSize);
const Color yuvColor = RGB2YUV(color);
const uint8_t& y = std::get<0>(yuvColor);
const uint8_t& u = std::get<1>(yuvColor);
const uint8_t& v = std::get<2>(yuvColor);
const Color expectation(v, y, u);
IsColorMatched(expectation, RGBX.get(), bufferSize);
}
}
// Fills a 4×4 Y plane and chroma planes for a frame whose luma is divided
// into four 2×2 blocks. aColors[blockRow][blockCol] gives the color for each
// block. Chroma plane dimensions depend on aSubsampling:
// FULL (YV24): 4×4 chroma, each pixel maps 1:1 to a luma pixel.
// HALF_WIDTH (YV16): 2×4 chroma, half-width but full height.
// HALF_WIDTH_AND_HEIGHT (YV12): 2×2 chroma.
// Callers must provide at least 16 bytes for aUBuf/aVBuf to cover the FULL
// case; smaller subsamplings use only a prefix of that.
static void FillTwoByTwoFrame(const Color aColors[2][2],
gfx::ChromaSubsampling aSubsampling,
uint8_t* aYBuf, uint8_t* aUBuf, uint8_t* aVBuf) {
// Give each luma pixel a unique Y by adding a small per-pixel offset based
// on its position within its 2x2 chroma block: +0/+2/+4/+6 for
// (top-left/top-right/bottom-left/bottom-right). This makes luma sampling
// bugs detectable without meaningfully shifting the color.
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
uint8_t baseY = std::get<0>(RGB2YUV(aColors[r / 2][c / 2]));
aYBuf[r * 4 + c] = baseY + (r % 2) * 4 + (c % 2) * 2;
}
}
int chromaWidth = (aSubsampling == gfx::ChromaSubsampling::FULL) ? 4 : 2;
int chromaHeight =
(aSubsampling == gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT) ? 2 : 4;
for (int chromaRow = 0; chromaRow < chromaHeight; chromaRow++) {
int blockRow = chromaRow * 2 / chromaHeight;
for (int chromaCol = 0; chromaCol < chromaWidth; chromaCol++) {
int blockCol = chromaCol * 2 / chromaWidth;
aUBuf[chromaRow * chromaWidth + chromaCol] =
std::get<1>(RGB2YUV(aColors[blockRow][blockCol]));
aVBuf[chromaRow * chromaWidth + chromaCol] =
std::get<2>(RGB2YUV(aColors[blockRow][blockCol]));
}
}
}
// Fills and converts a 4×4 test frame, writing the result into aOutput.
// aStride is in bytes. See FillTwoByTwoFrame for the aColors layout.
static void ConvertTestFrame(const Color aColors[2][2],
gfx::ChromaSubsampling aSubsampling,
const gfx::IntRect& aPictureRect, uint8_t* aOutput,
int32_t aStride) {
uint8_t yBuf[16], uBuf[16], vBuf[16];
FillTwoByTwoFrame(aColors, aSubsampling, yBuf, uBuf, vBuf);
layers::PlanarYCbCrData data;
data.mYChannel = yBuf;
data.mYStride = 4;
data.mYSkip = 0;
data.mCbChannel = uBuf;
data.mCrChannel = vBuf;
data.mCbCrStride = (aSubsampling == gfx::ChromaSubsampling::FULL) ? 4 : 2;
data.mCbSkip = 0;
data.mCrSkip = 0;
data.mChromaSubsampling = aSubsampling;
data.mYUVColorSpace = gfx::YUVColorSpace::BT709;
data.mColorRange = gfx::ColorRange::LIMITED;
data.mPictureRect = aPictureRect;
ConvertYCbCrToRGB32(data, gfx::SurfaceFormat::R8G8B8X8, aOutput, aStride,
nullptr);
}
// Tests for odd pic_x / pic_y offsets in YV12, YV16, and YV24.
//
// The 4x4 frame has four 2x2 chroma blocks with distinct mid-range colors.
// Within each block each luma pixel has a unique Y value (offset +0/+2/+4/+6),
// so both chroma and luma misalignment are detectable. The reference is a full
// even-aligned 4x4 conversion; each odd-crop output pixel is checked against
// its corresponding source position in that reference.
static const gfx::ChromaSubsampling kTestSubsamplings[] = {
gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT,
gfx::ChromaSubsampling::HALF_WIDTH, gfx::ChromaSubsampling::FULL};
static void RunOddPicTest(const Color aColors[2][2],
const gfx::IntRect& aRect) {
const int32_t stride = aRect.Width() * 4;
UniquePtr<uint8_t[]> output = MakeUnique<uint8_t[]>(aRect.Height() * stride);
auto exp = GetExpectedConvertedRGB();
const size_t bt709 = static_cast<size_t>(gfx::YUVColorSpace::BT709);
for (gfx::ChromaSubsampling subsampling : kTestSubsamplings) {
// fullRef: even-aligned 4x4 reference (no odd-offset ambiguity).
uint8_t fullRef[4 * 4 * 4];
ConvertTestFrame(aColors, subsampling, gfx::IntRect(0, 0, 4, 4), fullRef,
4 * 4);
// Sanity-check the reference: top-left of each 2x2 block (Y offset 0)
// must match GetExpectedConvertedRGB; the other three pixels in the block
// must be distinct but close (Y delta ≤6 → channel delta ~2-7).
for (int br = 0; br < 2; br++) {
for (int bc = 0; bc < 2; bc++) {
uint8_t* base = fullRef + (br * 2) * 4 * 4 + (bc * 2) * 4;
IsColorMatched(exp[Hash(aColors[br][bc])][bt709], base, 4);
for (int dr = 0; dr < 2; dr++) {
for (int dc = 0; dc < 2; dc++) {
if (dr == 0 && dc == 0) continue;
uint8_t* other =
fullRef + (br * 2 + dr) * 4 * 4 + (bc * 2 + dc) * 4;
for (int ch = 0; ch < 3; ch++) {
ASSERT_NE(base[ch], other[ch]);
ASSERT_NEAR(base[ch], other[ch], 10);
}
}
}
}
}
// output: the odd-crop conversion under test.
ConvertTestFrame(aColors, subsampling, aRect, output.get(), stride);
// Each output pixel must match its source position in the full reference.
for (int row = 0; row < aRect.Height(); row++) {
for (int col = 0; col < aRect.Width(); col++) {
uint8_t* ref = fullRef + (aRect.y + row) * 4 * 4 + (aRect.x + col) * 4;
Color expected(ref[0], ref[1], ref[2]);
IsColorMatched(expected, output.get() + row * stride + col * 4, 4);
}
}
}
}
TEST(YCbCrUtils, ConvertYCbCrToRGB32OddPicOffset)
{
const Color colors[2][2] = {{CHOCOLATE, PERU}, {ROSYBROWN, STEELBLUE}};
RunOddPicTest(colors, gfx::IntRect(1, 1, 3, 3)); // both odd
RunOddPicTest(colors, gfx::IntRect(1, 0, 3, 4)); // odd pic_x only
RunOddPicTest(colors, gfx::IntRect(0, 1, 4, 3)); // odd pic_y only
}