// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/gfx/color_analysis.h" #include #include #include #include "testing/gtest/include/gtest/gtest.h" #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkColor.h" #include "ui/gfx/canvas.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/image/image.h" namespace color_utils { const unsigned char k1x1White[] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b, 0x13, 0x01, 0x00, 0x9a, 0x9c, 0x18, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xdb, 0x02, 0x11, 0x15, 0x16, 0x1b, 0xaa, 0x58, 0x38, 0x76, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x00, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49, 0x4d, 0x50, 0x57, 0x81, 0x0e, 0x17, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xff, 0xff, 0x3f, 0x00, 0x05, 0xfe, 0x02, 0xfe, 0xdc, 0xcc, 0x59, 0xe7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 }; const unsigned char k1x3BlueWhite[] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x08, 0x02, 0x00, 0x00, 0x00, 0xdd, 0xbf, 0xf2, 0xd5, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b, 0x13, 0x01, 0x00, 0x9a, 0x9c, 0x18, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xdb, 0x02, 0x12, 0x01, 0x0a, 0x2c, 0xfd, 0x08, 0x64, 0x66, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x00, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49, 0x4d, 0x50, 0x57, 0x81, 0x0e, 0x17, 0x00, 0x00, 0x00, 0x14, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xff, 0xff, 0x3f, 0x13, 0x03, 0x03, 0x03, 0x03, 0x03, 0xc3, 0x7f, 0x00, 0x1e, 0xfd, 0x03, 0xff, 0xde, 0x72, 0x58, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 }; const unsigned char k1x3BlueRed[] = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03, 0x08, 0x02, 0x00, 0x00, 0x00, 0xdd, 0xbf, 0xf2, 0xd5, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0b, 0x13, 0x00, 0x00, 0x0b, 0x13, 0x01, 0x00, 0x9a, 0x9c, 0x18, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4d, 0x45, 0x07, 0xdb, 0x02, 0x12, 0x01, 0x07, 0x09, 0x03, 0xa2, 0xce, 0x6c, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x00, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49, 0x4d, 0x50, 0x57, 0x81, 0x0e, 0x17, 0x00, 0x00, 0x00, 0x14, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0xc0, 0xc4, 0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xf0, 0x1f, 0x00, 0x0c, 0x10, 0x02, 0x01, 0x2c, 0x8f, 0x8b, 0x8c, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82 }; const HSL kDefaultLowerBound = {-1, -1, 0.15}; const HSL kDefaultUpperBound = {-1, -1, 0.85}; // Creates a 1-dimensional png of the pixel colors found in |colors|. scoped_refptr CreateTestPNG( const std::vector& colors) { SkBitmap bitmap; bitmap.allocN32Pixels(colors.size(), 1); SkAutoLockPixels lock(bitmap); for (size_t i = 0; i < colors.size(); ++i) { bitmap.eraseArea(SkIRect::MakeXYWH(i, 0, 1, 1), colors[i]); } return gfx::Image::CreateFrom1xBitmap(bitmap).As1xPNGBytes(); } class MockKMeanImageSampler : public KMeanImageSampler { public: MockKMeanImageSampler() : current_result_index_(0) { } explicit MockKMeanImageSampler(const std::vector& samples) : prebaked_sample_results_(samples), current_result_index_(0) { } ~MockKMeanImageSampler() override {} void AddSample(int sample) { prebaked_sample_results_.push_back(sample); } int GetSample(int width, int height) override { if (current_result_index_ >= prebaked_sample_results_.size()) { current_result_index_ = 0; } if (prebaked_sample_results_.empty()) { return 0; } return prebaked_sample_results_[current_result_index_++]; } protected: std::vector prebaked_sample_results_; size_t current_result_index_; }; // Return true if a color channel is approximately equal to an expected value. bool ChannelApproximatelyEqual(int expected, uint8_t channel) { return (abs(expected - static_cast(channel)) <= 1); } // Compute minimal and maximal graylevel (or alphalevel) of the input |bitmap|. // |bitmap| has to be allocated and configured to kA8_Config. void Calculate8bitBitmapMinMax(const SkBitmap& bitmap, uint8_t* min_gl, uint8_t* max_gl) { SkAutoLockPixels bitmap_lock(bitmap); DCHECK(bitmap.getPixels()); DCHECK_EQ(bitmap.colorType(), kAlpha_8_SkColorType); DCHECK(min_gl); DCHECK(max_gl); *min_gl = std::numeric_limits::max(); *max_gl = std::numeric_limits::min(); for (int y = 0; y < bitmap.height(); ++y) { uint8_t* current_color = bitmap.getAddr8(0, y); for (int x = 0; x < bitmap.width(); ++x, ++current_color) { *min_gl = std::min(*min_gl, *current_color); *max_gl = std::max(*max_gl, *current_color); } } } class ColorAnalysisTest : public testing::Test { }; TEST_F(ColorAnalysisTest, CalculatePNGKMeanAllWhite) { MockKMeanImageSampler test_sampler; test_sampler.AddSample(0); scoped_refptr png( new base::RefCountedBytes( std::vector( k1x1White, k1x1White + sizeof(k1x1White) / sizeof(unsigned char)))); SkColor color = CalculateKMeanColorOfPNG( png, kDefaultLowerBound, kDefaultUpperBound, &test_sampler); EXPECT_EQ(color, SK_ColorWHITE); } TEST_F(ColorAnalysisTest, CalculatePNGKMeanIgnoreWhiteLightness) { MockKMeanImageSampler test_sampler; test_sampler.AddSample(0); test_sampler.AddSample(1); test_sampler.AddSample(2); scoped_refptr png( new base::RefCountedBytes( std::vector( k1x3BlueWhite, k1x3BlueWhite + sizeof(k1x3BlueWhite) / sizeof(unsigned char)))); SkColor color = CalculateKMeanColorOfPNG( png, kDefaultLowerBound, kDefaultUpperBound, &test_sampler); EXPECT_EQ(SkColorSetARGB(0xFF, 0x00, 0x00, 0xFF), color); } TEST_F(ColorAnalysisTest, CalculatePNGKMeanPickMostCommon) { MockKMeanImageSampler test_sampler; test_sampler.AddSample(0); test_sampler.AddSample(1); test_sampler.AddSample(2); scoped_refptr png( new base::RefCountedBytes( std::vector( k1x3BlueRed, k1x3BlueRed + sizeof(k1x3BlueRed) / sizeof(unsigned char)))); SkColor color = CalculateKMeanColorOfPNG( png, kDefaultLowerBound, kDefaultUpperBound, &test_sampler); EXPECT_EQ(SkColorSetARGB(0xFF, 0xFF, 0x00, 0x00), color); } TEST_F(ColorAnalysisTest, CalculatePNGKMeanIgnoreRedHue) { MockKMeanImageSampler test_sampler; test_sampler.AddSample(0); test_sampler.AddSample(1); test_sampler.AddSample(2); std::vector colors(4, SK_ColorRED); colors[1] = SK_ColorBLUE; scoped_refptr png = CreateTestPNG(colors); HSL lower = {0.2, -1, 0.15}; HSL upper = {0.8, -1, 0.85}; SkColor color = CalculateKMeanColorOfPNG( png, lower, upper, &test_sampler); EXPECT_EQ(SK_ColorBLUE, color); } TEST_F(ColorAnalysisTest, CalculatePNGKMeanIgnoreGreySaturation) { MockKMeanImageSampler test_sampler; test_sampler.AddSample(0); test_sampler.AddSample(1); test_sampler.AddSample(2); std::vector colors(4, SK_ColorGRAY); colors[1] = SK_ColorBLUE; scoped_refptr png = CreateTestPNG(colors); HSL lower = {-1, 0.3, -1}; HSL upper = {-1, 1, -1}; SkColor color = CalculateKMeanColorOfPNG( png, lower, upper, &test_sampler); EXPECT_EQ(SK_ColorBLUE, color); } TEST_F(ColorAnalysisTest, GridSampler) { GridSampler sampler; const int kWidth = 16; const int kHeight = 16; // Sample starts at 1,1. EXPECT_EQ(1 + 1 * kWidth, sampler.GetSample(kWidth, kHeight)); EXPECT_EQ(1 + 4 * kWidth, sampler.GetSample(kWidth, kHeight)); EXPECT_EQ(1 + 7 * kWidth, sampler.GetSample(kWidth, kHeight)); EXPECT_EQ(1 + 10 * kWidth, sampler.GetSample(kWidth, kHeight)); // Step over by 3. EXPECT_EQ(4 + 1 * kWidth, sampler.GetSample(kWidth, kHeight)); EXPECT_EQ(4 + 4 * kWidth, sampler.GetSample(kWidth, kHeight)); EXPECT_EQ(4 + 7 * kWidth, sampler.GetSample(kWidth, kHeight)); EXPECT_EQ(4 + 10 * kWidth, sampler.GetSample(kWidth, kHeight)); } TEST_F(ColorAnalysisTest, FindClosestColor) { // Empty image returns input color. SkColor color = FindClosestColor(NULL, 0, 0, SK_ColorRED); EXPECT_EQ(SK_ColorRED, color); // Single color image returns that color. SkBitmap bitmap; bitmap.allocN32Pixels(16, 16); bitmap.eraseColor(SK_ColorWHITE); color = FindClosestColor(static_cast(bitmap.getPixels()), bitmap.width(), bitmap.height(), SK_ColorRED); EXPECT_EQ(SK_ColorWHITE, color); // Write a black pixel into the image. A dark grey input pixel should match // the black one in the image. uint32_t* pixel = bitmap.getAddr32(0, 0); *pixel = SK_ColorBLACK; color = FindClosestColor(static_cast(bitmap.getPixels()), bitmap.width(), bitmap.height(), SK_ColorDKGRAY); EXPECT_EQ(SK_ColorBLACK, color); } TEST_F(ColorAnalysisTest, CalculateKMeanColorOfBitmap) { // Create a 16x16 bitmap to represent a favicon. SkBitmap bitmap; bitmap.allocN32Pixels(16, 16); bitmap.eraseARGB(255, 100, 150, 200); SkColor color = CalculateKMeanColorOfBitmap(bitmap); EXPECT_EQ(255u, SkColorGetA(color)); // Color values are not exactly equal due to reversal of premultiplied alpha. EXPECT_TRUE(ChannelApproximatelyEqual(100, SkColorGetR(color))); EXPECT_TRUE(ChannelApproximatelyEqual(150, SkColorGetG(color))); EXPECT_TRUE(ChannelApproximatelyEqual(200, SkColorGetB(color))); // Test a bitmap with an alpha channel. bitmap.eraseARGB(128, 100, 150, 200); color = CalculateKMeanColorOfBitmap(bitmap); // Alpha channel should be ignored for dominant color calculation. EXPECT_EQ(255u, SkColorGetA(color)); EXPECT_TRUE(ChannelApproximatelyEqual(100, SkColorGetR(color))); EXPECT_TRUE(ChannelApproximatelyEqual(150, SkColorGetG(color))); EXPECT_TRUE(ChannelApproximatelyEqual(200, SkColorGetB(color))); } TEST_F(ColorAnalysisTest, ComputeColorCovarianceTrivial) { SkBitmap bitmap; bitmap.setInfo(SkImageInfo::MakeN32Premul(100, 200)); EXPECT_EQ(gfx::Matrix3F::Zeros(), ComputeColorCovariance(bitmap)); bitmap.allocPixels(); bitmap.eraseARGB(255, 50, 150, 200); gfx::Matrix3F covariance = ComputeColorCovariance(bitmap); // The answer should be all zeros. EXPECT_TRUE(covariance == gfx::Matrix3F::Zeros()); } TEST_F(ColorAnalysisTest, ComputeColorCovarianceWithCanvas) { gfx::Canvas canvas(gfx::Size(250, 200), 1.0f, true); // The image consists of vertical stripes, with color bands set to 100 // in overlapping stripes 150 pixels wide. canvas.FillRect(gfx::Rect(0, 0, 50, 200), SkColorSetRGB(100, 0, 0)); canvas.FillRect(gfx::Rect(50, 0, 50, 200), SkColorSetRGB(100, 100, 0)); canvas.FillRect(gfx::Rect(100, 0, 50, 200), SkColorSetRGB(100, 100, 100)); canvas.FillRect(gfx::Rect(150, 0, 50, 200), SkColorSetRGB(0, 100, 100)); canvas.FillRect(gfx::Rect(200, 0, 50, 200), SkColorSetRGB(0, 0, 100)); SkBitmap bitmap = skia::ReadPixels(canvas.sk_canvas()); gfx::Matrix3F covariance = ComputeColorCovariance(bitmap); gfx::Matrix3F expected_covariance = gfx::Matrix3F::Zeros(); expected_covariance.set(2400, 400, -1600, 400, 2400, 400, -1600, 400, 2400); EXPECT_EQ(expected_covariance, covariance); } TEST_F(ColorAnalysisTest, ApplyColorReductionSingleColor) { // The test runs color reduction on a single-colot image, where results are // bound to be uninteresting. This is an important edge case, though. SkBitmap source, result; source.allocN32Pixels(300, 200); result.allocPixels(SkImageInfo::MakeA8(300, 200)); source.eraseARGB(255, 50, 150, 200); gfx::Vector3dF transform(1.0f, .5f, 0.1f); // This transform, if not scaled, should result in GL=145. EXPECT_TRUE(ApplyColorReduction(source, transform, false, &result)); uint8_t min_gl = 0; uint8_t max_gl = 0; Calculate8bitBitmapMinMax(result, &min_gl, &max_gl); EXPECT_EQ(145, min_gl); EXPECT_EQ(145, max_gl); // Now scan requesting rescale. Expect all 0. EXPECT_TRUE(ApplyColorReduction(source, transform, true, &result)); Calculate8bitBitmapMinMax(result, &min_gl, &max_gl); EXPECT_EQ(0, min_gl); EXPECT_EQ(0, max_gl); // Test cliping to upper limit. transform.set_z(1.1f); EXPECT_TRUE(ApplyColorReduction(source, transform, false, &result)); Calculate8bitBitmapMinMax(result, &min_gl, &max_gl); EXPECT_EQ(0xFF, min_gl); EXPECT_EQ(0xFF, max_gl); // Test cliping to upper limit. transform.Scale(-1.0f); EXPECT_TRUE(ApplyColorReduction(source, transform, false, &result)); Calculate8bitBitmapMinMax(result, &min_gl, &max_gl); EXPECT_EQ(0x0, min_gl); EXPECT_EQ(0x0, max_gl); } TEST_F(ColorAnalysisTest, ApplyColorReductionBlackAndWhite) { // Check with images with multiple colors. This is really different only when // the result is scaled. gfx::Canvas canvas(gfx::Size(300, 200), 1.0f, true); // The image consists of vertical non-overlapping stripes 150 pixels wide. canvas.FillRect(gfx::Rect(0, 0, 150, 200), SkColorSetRGB(0, 0, 0)); canvas.FillRect(gfx::Rect(150, 0, 150, 200), SkColorSetRGB(255, 255, 255)); SkBitmap source = skia::ReadPixels(canvas.sk_canvas()); SkBitmap result; result.allocPixels(SkImageInfo::MakeA8(300, 200)); gfx::Vector3dF transform(1.0f, 0.5f, 0.1f); EXPECT_TRUE(ApplyColorReduction(source, transform, true, &result)); uint8_t min_gl = 0; uint8_t max_gl = 0; Calculate8bitBitmapMinMax(result, &min_gl, &max_gl); EXPECT_EQ(0, min_gl); EXPECT_EQ(255, max_gl); EXPECT_EQ(min_gl, SkColorGetA(result.getColor(0, 0))); EXPECT_EQ(max_gl, SkColorGetA(result.getColor(299, 199))); // Reverse test. transform.Scale(-1.0f); EXPECT_TRUE(ApplyColorReduction(source, transform, true, &result)); min_gl = 0; max_gl = 0; Calculate8bitBitmapMinMax(result, &min_gl, &max_gl); EXPECT_EQ(0, min_gl); EXPECT_EQ(255, max_gl); EXPECT_EQ(max_gl, SkColorGetA(result.getColor(0, 0))); EXPECT_EQ(min_gl, SkColorGetA(result.getColor(299, 199))); } TEST_F(ColorAnalysisTest, ApplyColorReductionMultiColor) { // Check with images with multiple colors. This is really different only when // the result is scaled. gfx::Canvas canvas(gfx::Size(300, 200), 1.0f, true); // The image consists of vertical non-overlapping stripes 100 pixels wide. canvas.FillRect(gfx::Rect(0, 0, 100, 200), SkColorSetRGB(100, 0, 0)); canvas.FillRect(gfx::Rect(100, 0, 100, 200), SkColorSetRGB(0, 255, 0)); canvas.FillRect(gfx::Rect(200, 0, 100, 200), SkColorSetRGB(0, 0, 128)); SkBitmap source = skia::ReadPixels(canvas.sk_canvas()); SkBitmap result; result.allocPixels(SkImageInfo::MakeA8(300, 200)); gfx::Vector3dF transform(1.0f, 0.5f, 0.1f); EXPECT_TRUE(ApplyColorReduction(source, transform, false, &result)); uint8_t min_gl = 0; uint8_t max_gl = 0; Calculate8bitBitmapMinMax(result, &min_gl, &max_gl); EXPECT_EQ(12, min_gl); EXPECT_EQ(127, max_gl); EXPECT_EQ(min_gl, SkColorGetA(result.getColor(299, 199))); EXPECT_EQ(max_gl, SkColorGetA(result.getColor(150, 0))); EXPECT_EQ(100U, SkColorGetA(result.getColor(0, 0))); EXPECT_TRUE(ApplyColorReduction(source, transform, true, &result)); Calculate8bitBitmapMinMax(result, &min_gl, &max_gl); EXPECT_EQ(0, min_gl); EXPECT_EQ(255, max_gl); EXPECT_EQ(min_gl, SkColorGetA(result.getColor(299, 199))); EXPECT_EQ(max_gl, SkColorGetA(result.getColor(150, 0))); EXPECT_EQ(193U, SkColorGetA(result.getColor(0, 0))); } TEST_F(ColorAnalysisTest, ComputePrincipalComponentImageNotComputable) { SkBitmap source, result; source.allocN32Pixels(300, 200); result.allocPixels(SkImageInfo::MakeA8(300, 200)); source.eraseARGB(255, 50, 150, 200); // This computation should fail since all colors always vary together. EXPECT_FALSE(ComputePrincipalComponentImage(source, &result)); } TEST_F(ColorAnalysisTest, ComputePrincipalComponentImage) { gfx::Canvas canvas(gfx::Size(300, 200), 1.0f, true); // The image consists of vertical non-overlapping stripes 100 pixels wide. canvas.FillRect(gfx::Rect(0, 0, 100, 200), SkColorSetRGB(10, 10, 10)); canvas.FillRect(gfx::Rect(100, 0, 100, 200), SkColorSetRGB(100, 100, 100)); canvas.FillRect(gfx::Rect(200, 0, 100, 200), SkColorSetRGB(255, 255, 255)); SkBitmap source = skia::ReadPixels(canvas.sk_canvas()); SkBitmap result; result.allocPixels(SkImageInfo::MakeA8(300, 200)); // This computation should fail since all colors always vary together. EXPECT_TRUE(ComputePrincipalComponentImage(source, &result)); uint8_t min_gl = 0; uint8_t max_gl = 0; Calculate8bitBitmapMinMax(result, &min_gl, &max_gl); EXPECT_EQ(0, min_gl); EXPECT_EQ(255, max_gl); EXPECT_EQ(min_gl, SkColorGetA(result.getColor(0, 0))); EXPECT_EQ(max_gl, SkColorGetA(result.getColor(299, 199))); EXPECT_EQ(93U, SkColorGetA(result.getColor(150, 0))); } } // namespace color_utils