Commit 8bac8fb4 authored by Yuri Wiitala's avatar Yuri Wiitala Committed by Commit Bot

GLScaler: Add gamma-aware scaling, for improved color accuracy.

Adds the implementation to support gamma-aware scaling: It uses half-
floats to increase the precision of the color values, and adds an
"import stage" to linearize the color values before any texture sampling
or scaling occurs.

Added a pixel test to prove the gamma-aware versus non-gamma-aware
configuration of GLScaler produces the expected results.

Bug: 870036, 810131
Change-Id: Ie68121add789169dbda0a578f03a257d089d4f6c
Reviewed-on: https://chromium-review.googlesource.com/c/1297540
Commit-Queue: Yuri Wiitala <miu@chromium.org>
Reviewed-by: default avatarXiangjun Zhang <xjz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#603797}
parent 1a0a3501
...@@ -22,8 +22,9 @@ specific_include_rules = { ...@@ -22,8 +22,9 @@ specific_include_rules = {
"+gpu/ipc/common", "+gpu/ipc/common",
"+third_party/skia", "+third_party/skia",
], ],
".*(_unittest|_pixeltest)\.cc": [ ".*(_unittest|_pixeltest|test_util)\.cc": [
"+cc/test", "+cc/test",
"+components/viz/test",
"+gpu/ipc/gl_in_process_context.h", "+gpu/ipc/gl_in_process_context.h",
"+media/base", "+media/base",
"+third_party/skia/include/core", "+third_party/skia/include/core",
......
...@@ -141,10 +141,13 @@ bool GLScaler::Configure(const Parameters& new_params) { ...@@ -141,10 +141,13 @@ bool GLScaler::Configure(const Parameters& new_params) {
} }
} }
// Color space transformation is meaningless when using the deinterleaver. // Color space transformation is meaningless when using the deinterleaver
// because it only deals with two color channels. This also means precise
// color management must be disabled.
if (params_.export_format == if (params_.export_format ==
Parameters::ExportFormat::DEINTERLEAVE_PAIRWISE && Parameters::ExportFormat::DEINTERLEAVE_PAIRWISE &&
params_.source_color_space != params_.output_color_space) { (params_.source_color_space != params_.output_color_space ||
params_.enable_precise_color_management)) {
NOTIMPLEMENTED(); NOTIMPLEMENTED();
return false; return false;
} }
...@@ -173,10 +176,33 @@ bool GLScaler::Configure(const Parameters& new_params) { ...@@ -173,10 +176,33 @@ bool GLScaler::Configure(const Parameters& new_params) {
} }
chain = MaybeAppendExportStage(gl, std::move(chain), params_.export_format); chain = MaybeAppendExportStage(gl, std::move(chain), params_.export_format);
// TODO(crbug.com/870036): Add support for color management (uses half-float // Determine the color space and the data type of the pixels in the
// textures). // intermediate textures, depending on whether precise color management is
scaling_color_space_ = params_.source_color_space; // enabled. Note that nothing special need be done here if no scaling will be
const GLenum intermediate_texture_type = GL_UNSIGNED_BYTE; // performed.
GLenum intermediate_texture_type;
if (params_.enable_precise_color_management &&
params_.scale_from != params_.scale_to) {
// Ensure the scaling color space is using a linear transfer function.
constexpr auto kLinearFunction = std::make_tuple(1, 0, 1, 0, 0, 0, 1);
SkColorSpaceTransferFn fn;
if (params_.source_color_space.GetTransferFunction(&fn) &&
std::make_tuple(fn.fA, fn.fB, fn.fC, fn.fD, fn.fE, fn.fF, fn.fG) ==
kLinearFunction) {
scaling_color_space_ = params_.source_color_space;
} else {
// Use the source color space, but with a linear transfer function.
SkMatrix44 to_XYZD50;
params_.source_color_space.GetPrimaryMatrix(&to_XYZD50);
std::tie(fn.fA, fn.fB, fn.fC, fn.fD, fn.fE, fn.fF, fn.fG) =
kLinearFunction;
scaling_color_space_ = gfx::ColorSpace::CreateCustom(to_XYZD50, fn);
}
intermediate_texture_type = GL_HALF_FLOAT_OES;
} else {
scaling_color_space_ = params_.source_color_space;
intermediate_texture_type = GL_UNSIGNED_BYTE;
}
// Set the shader program on the final stage. Include color space // Set the shader program on the final stage. Include color space
// transformation and swizzling, if necessary. // transformation and swizzling, if necessary.
...@@ -209,6 +235,30 @@ bool GLScaler::Configure(const Parameters& new_params) { ...@@ -209,6 +235,30 @@ bool GLScaler::Configure(const Parameters& new_params) {
// From this point, |input_stage| points to the first ScalerStage (i.e., the // From this point, |input_stage| points to the first ScalerStage (i.e., the
// one that will be reading from the source). // one that will be reading from the source).
// If necessary, prepend an extra "import stage" that color-converts the input
// before any scaling occurs. It's important not to merge color space
// conversion of the source with any other steps because the texture sampler
// must not linearly interpolate until after the colors have been mapped to a
// linear color space.
if (params_.source_color_space != scaling_color_space_) {
input_stage->set_input_stage(std::make_unique<ScalerStage>(
gl, Shader::BILINEAR, HORIZONTAL, input_stage->scale_from(),
input_stage->scale_from()));
input_stage = input_stage->input_stage();
transform = gfx::ColorTransform::NewColorTransform(
params_.source_color_space, scaling_color_space_,
gfx::ColorTransform::Intent::INTENT_PERCEPTUAL);
if (!transform->CanGetShaderSource()) {
NOTIMPLEMENTED() << "color transform from "
<< params_.source_color_space.ToString() << " to "
<< scaling_color_space_.ToString();
return false;
}
input_stage->set_shader_program(
GetShaderProgram(input_stage->shader(), intermediate_texture_type,
transform.get(), kNoSwizzle));
}
// If the source content is Y-flipped, the input scaler stage will perform // If the source content is Y-flipped, the input scaler stage will perform
// math to account for this. It also will flip the content during scaling so // math to account for this. It also will flip the content during scaling so
// that all following stages may assume the content is not flipped. Then, the // that all following stages may assume the content is not flipped. Then, the
...@@ -1519,6 +1569,12 @@ std::ostream& operator<<(std::ostream& out, const GLScaler& scaler) { ...@@ -1519,6 +1569,12 @@ std::ostream& operator<<(std::ostream& out, const GLScaler& scaler) {
out << ' ' << stage->scale_from().ToString() << " to " out << ' ' << stage->scale_from().ToString() << " to "
<< stage->scale_to().ToString(); << stage->scale_to().ToString();
} }
if (!stage->input_stage() &&
scaler.params_.source_color_space != scaler.scaling_color_space_) {
out << ", with color x-form "
<< scaler.params_.source_color_space.ToString() << " to "
<< scaler.scaling_color_space_.ToString();
}
if (stage == final_stage) { if (stage == final_stage) {
if (scaler.params_.output_color_space != scaler.scaling_color_space_) { if (scaler.params_.output_color_space != scaler.scaling_color_space_) {
out << ", with color x-form to " out << ", with color x-form to "
......
...@@ -363,6 +363,7 @@ TEST_F(GLScalerPixelTest, Example_ScaleAndExportForScreenVideoCapture) { ...@@ -363,6 +363,7 @@ TEST_F(GLScalerPixelTest, Example_ScaleAndExportForScreenVideoCapture) {
params.scale_to = gfx::Vector2d(1280, 720); params.scale_to = gfx::Vector2d(1280, 720);
params.source_color_space = DefaultRGBColorSpace(); params.source_color_space = DefaultRGBColorSpace();
params.output_color_space = DefaultYUVColorSpace(); params.output_color_space = DefaultYUVColorSpace();
params.enable_precise_color_management = true;
params.quality = GLScaler::Parameters::Quality::GOOD; params.quality = GLScaler::Parameters::Quality::GOOD;
params.is_flipped_source = true; params.is_flipped_source = true;
params.flip_output = true; params.flip_output = true;
...@@ -372,9 +373,11 @@ TEST_F(GLScalerPixelTest, Example_ScaleAndExportForScreenVideoCapture) { ...@@ -372,9 +373,11 @@ TEST_F(GLScalerPixelTest, Example_ScaleAndExportForScreenVideoCapture) {
ASSERT_TRUE(scaler()->Configure(params)); ASSERT_TRUE(scaler()->Configure(params));
EXPECT_STRING_MATCHES( EXPECT_STRING_MATCHES(
u8"Output " u8"Output "
u8"← {I422_NV61_MRT/lowp [5120 720] to [1280 720], with color x-form " u8"← {I422_NV61_MRT/mediump [5120 720] to [1280 720], with color x-form "
u8"to *BT709*, with swizzle(0)} " u8"to *BT709*, with swizzle(0)} "
u8"← {BILINEAR2/lowp+flip_y [2160 1440] to [1280 720]} " u8"← {BILINEAR2/mediump [2160 1440] to [1280 720]} "
u8"← {BILINEAR/mediump+flip_y copy, with color x-form *BT709* to "
u8"*transfer:1.0000\\*x*} "
u8"← Source", u8"← Source",
GetScalerString()); GetScalerString());
...@@ -450,6 +453,82 @@ TEST_F(GLScalerPixelTest, Example_ScaleAndExportForScreenVideoCapture) { ...@@ -450,6 +453,82 @@ TEST_F(GLScalerPixelTest, Example_ScaleAndExportForScreenVideoCapture) {
<< "\nExpected: " << cc::GetPNGDataUrl(expected); << "\nExpected: " << cc::GetPNGDataUrl(expected);
} }
// Performs a scaling-with-gamma-correction experiment to test GLScaler's
// "precise color management" feature. A 50% scale is executed on the same
// source image, once with color management turned on, and once with it turned
// off. The results, each of which should be different, are then examined.
TEST_F(GLScalerPixelTest, ScalesWithColorManagement) {
if (!scaler()->SupportsPreciseColorManagement()) {
LOG(WARNING) << "Skipping test due to lack of 16-bit float support.";
return;
}
// An image of a raspberry (source:
// https://commons.wikimedia.org/wiki/File:Framboise_Margy_3.jpg) has been
// transformed in such a way that scaling it by half in both directions will
// reveal whether scaling is occurring on linearized color values. When scaled
// correctly, the output image should contain a visible raspberry blended
// heavily with solid gray. However, if done naively, the output will be a
// solid 50% gray. For details, see: http://www.ericbrasseur.org/gamma.html
//
// Note that the |source| and |expected| images both use the sRGB color space.
const SkBitmap source = LoadPNGTestImage("rasp-grayator.png");
ASSERT_FALSE(source.isNull());
const SkBitmap expected = LoadPNGTestImage("rasp-grayator-half.png");
ASSERT_FALSE(expected.isNull());
const gfx::Size output_size =
gfx::Size(source.width() / 2, source.height() / 2);
ASSERT_EQ(gfx::Size(expected.width(), expected.height()), output_size);
const SkBitmap expected_naive = AllocateRGBABitmap(output_size);
expected_naive.eraseColor(SkColorSetARGB(0xff, 0x7f, 0x7f, 0x7f));
// Scale the right way: With color management enabled, the raspberry should be
// visible in the downscaled result.
GLScaler::Parameters params;
params.scale_from = gfx::Vector2d(2, 2);
params.scale_to = gfx::Vector2d(1, 1);
params.source_color_space = gfx::ColorSpace::CreateSRGB();
params.enable_precise_color_management = true;
params.quality = GLScaler::Parameters::Quality::GOOD;
params.is_flipped_source = false;
ASSERT_TRUE(scaler()->Configure(params));
EXPECT_STRING_MATCHES(
u8"Output "
u8"← {BILINEAR/mediump [2 2] to [1 1], with color x-form to *BT709*} "
u8"← {BILINEAR/mediump copy, with color x-form *BT709* to "
u8"*transfer:1.0000\\*x*} "
u8"← Source",
GetScalerString());
const SkBitmap actual =
Scale(source, gfx::Vector2d(), gfx::Rect(output_size));
constexpr float kAvgAbsoluteErrorLimit = 1.f;
constexpr int kMaxAbsoluteErrorLimit = 2;
EXPECT_TRUE(cc::FuzzyPixelComparator(
false, 100.f, 0.f,
GetBaselineColorDifference() + kAvgAbsoluteErrorLimit,
GetBaselineColorDifference() + kMaxAbsoluteErrorLimit, 0)
.Compare(expected, actual))
<< "\nActual: " << cc::GetPNGDataUrl(actual)
<< "\nExpected (half size): " << cc::GetPNGDataUrl(expected)
<< "\nOriginal: " << cc::GetPNGDataUrl(source);
// Scale the naive way: Without color management, expect a solid gray result.
params.enable_precise_color_management = false;
ASSERT_TRUE(scaler()->Configure(params));
EXPECT_EQ(u8"Output ← {BILINEAR/lowp [2 2] to [1 1]} ← Source",
GetScalerString());
const SkBitmap actual_naive =
Scale(source, gfx::Vector2d(), gfx::Rect(output_size));
EXPECT_TRUE(cc::FuzzyPixelComparator(
false, 100.f, 0.f,
GetBaselineColorDifference() + kAvgAbsoluteErrorLimit,
GetBaselineColorDifference() + kMaxAbsoluteErrorLimit, 0)
.Compare(expected_naive, actual_naive))
<< "\nActual: " << cc::GetPNGDataUrl(actual_naive)
<< "\nExpected (half size): " << cc::GetPNGDataUrl(expected_naive)
<< "\nOriginal: " << cc::GetPNGDataUrl(source);
}
#undef EXPECT_STRING_MATCHES #undef EXPECT_STRING_MATCHES
} // namespace viz } // namespace viz
...@@ -7,7 +7,11 @@ ...@@ -7,7 +7,11 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include "base/files/file_path.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/path_service.h"
#include "cc/test/pixel_test_utils.h"
#include "components/viz/test/paths.h"
#include "third_party/skia/include/core/SkColor.h" #include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkImageInfo.h" #include "third_party/skia/include/core/SkImageInfo.h"
#include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/rect.h"
...@@ -315,6 +319,27 @@ SkBitmap GLScalerTestUtil::CreateVerticallyFlippedBitmap( ...@@ -315,6 +319,27 @@ SkBitmap GLScalerTestUtil::CreateVerticallyFlippedBitmap(
return bitmap; return bitmap;
} }
// static
SkBitmap GLScalerTestUtil::LoadPNGTestImage(const std::string& basename) {
base::FilePath test_dir;
if (!base::PathService::Get(Paths::DIR_TEST_DATA, &test_dir)) {
LOG(ERROR) << "Unable to get Paths::DIR_TEST_DATA from base::PathService.";
return SkBitmap();
}
const auto source_file = test_dir.AppendASCII(basename);
SkBitmap as_n32;
if (!cc::ReadPNGFile(source_file, &as_n32)) {
return SkBitmap();
}
SkBitmap as_rgba =
AllocateRGBABitmap(gfx::Size(as_n32.width(), as_n32.height()));
if (!as_n32.readPixels(SkPixmap(as_rgba.info(), as_rgba.getAddr(0, 0),
as_rgba.rowBytes()))) {
return SkBitmap();
}
return as_rgba;
}
// The area and color of the bars in a 1920x1080 HD SMPTE color bars test image // The area and color of the bars in a 1920x1080 HD SMPTE color bars test image
// (https://commons.wikimedia.org/wiki/File:SMPTE_Color_Bars_16x9.svg). The gray // (https://commons.wikimedia.org/wiki/File:SMPTE_Color_Bars_16x9.svg). The gray
// linear gradient bar is defined as half solid 0-level black and half solid // linear gradient bar is defined as half solid 0-level black and half solid
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
#include <stdint.h> #include <stdint.h>
#include <string>
#include <vector> #include <vector>
#include "base/macros.h" #include "base/macros.h"
...@@ -109,6 +110,10 @@ class GLScalerTestUtil { ...@@ -109,6 +110,10 @@ class GLScalerTestUtil {
// Returns the |source| bitmap, but with its content vertically flipped. // Returns the |source| bitmap, but with its content vertically flipped.
static SkBitmap CreateVerticallyFlippedBitmap(const SkBitmap& source); static SkBitmap CreateVerticallyFlippedBitmap(const SkBitmap& source);
// Loads a PNG test image from the test directory, and converts it to the same
// SkImageInfo format used by AllocateRGBABitmap() (i.e., GL_RGBA byte order).
static SkBitmap LoadPNGTestImage(const std::string& basename);
// The area and color of the bars in a 1920x1080 HD SMPTE color bars test // The area and color of the bars in a 1920x1080 HD SMPTE color bars test
// image (https://commons.wikimedia.org/wiki/File:SMPTE_Color_Bars_16x9.svg). // image (https://commons.wikimedia.org/wiki/File:SMPTE_Color_Bars_16x9.svg).
// The gray linear gradient bar is defined as half solid 0-level black and // The gray linear gradient bar is defined as half solid 0-level black and
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment