blob: f42b1611e25c663d93a718d9f3442faebd68edd5 [file] [log] [blame]
// Copyright 2019 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 "services/image_annotation/public/cpp/image_processor.h"
#include <cmath>
#include <limits>
#include "base/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "services/image_annotation/image_annotation_metrics.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/src/core/SkEndian.h"
#include "ui/gfx/codec/jpeg_codec.h"
namespace image_annotation {
namespace {
using testing::Eq;
using testing::Lt;
constexpr double kMaxError = 1e-6;
// Generates an image of size |dim|x|dim| containing an 8x8 black and white
// checkerboard pattern.
SkBitmap GenCheckerboardBitmap(const int dim) {
const int check_dim = dim / 8;
SkBitmap out;
out.setInfo(SkImageInfo::Make(dim, dim, kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType));
out.allocPixels();
uint8_t* const pixels = reinterpret_cast<uint8_t*>(out.getPixels());
for (int row = 0; row < dim; ++row) {
for (int col = 0; col < dim; ++col) {
const bool black = ((row / check_dim + col / check_dim) % 2) == 1;
uint8_t* const byte_pos =
pixels + row * out.rowBytes() + col * out.bytesPerPixel();
// RGBA refers to big endian ordering.
*reinterpret_cast<uint32_t*>(byte_pos) =
black ? SkEndian_SwapBE32(0x000000FF) : 0xFFFFFFFF;
}
}
return out;
}
// Returns the mean sum of squared distance between each channel of each pixel
// in the original and compressed images.
double CalcImageError(const SkBitmap& orig, const SkBitmap& comp) {
// Only valid to call on images of matching size.
CHECK(orig.width() == comp.width() && orig.height() == comp.height());
double sum = 0;
for (int row = 0; row < orig.width(); ++row) {
for (int col = 0; col < orig.height(); ++col) {
const auto orig_col = SkColor4f::FromColor(orig.getColor(col, row));
const auto comp_col = SkColor4f::FromColor(comp.getColor(col, row));
for (int i = 0; i < 4; ++i) {
sum += std::pow(orig_col.vec()[i] - comp_col.vec()[i], 2);
}
}
}
return sum / (4 * orig.width() * orig.height());
}
// Takes an expected image and the actual image produced, and outputs the
// mean sum of squared distance between their pixels.
void OutputImageError(double* const error,
const SkBitmap& expected,
const std::vector<uint8_t>& result,
const int32_t width,
const int32_t height) {
const std::unique_ptr<SkBitmap> comp =
gfx::JPEGCodec::Decode(result.data(), result.size());
CHECK(comp);
*error = width == expected.width() && height == expected.height()
? CalcImageError(expected, *comp)
: std::numeric_limits<double>::infinity();
}
} // namespace
TEST(ImageProcessorTest, NullImage) {
base::test::TaskEnvironment test_task_env;
base::HistogramTester histogram_tester;
bool empty_bytes = false;
// The "get pixels" callback returns a null image, simulating failure to fetch
// pixels.
ImageProcessor(base::BindRepeating([]() { return SkBitmap(); }))
.GetJpgImageData(base::BindOnce(
[](bool* const empty_bytes, const std::vector<uint8_t>& bytes,
const int32_t w, const int32_t h) {
*empty_bytes = bytes.empty() && w == 0 && h == 0;
},
&empty_bytes));
test_task_env.RunUntilIdle();
EXPECT_THAT(empty_bytes, Eq(true));
histogram_tester.ExpectUniqueSample(metrics_internal::kSourcePixelCount,
0 /* sample */, 1 /* count */);
}
TEST(ImageProcessorTest, ImageContent) {
base::test::TaskEnvironment test_task_env;
base::HistogramTester histogram_tester;
// Create one image that doesn't need scaling and one image that does.
const int max_dim = static_cast<int>(std::sqrt(ImageProcessor::kMaxPixels));
const SkBitmap small_orig = GenCheckerboardBitmap(max_dim);
const SkBitmap large_orig = GenCheckerboardBitmap(max_dim * 2);
// Process the image that doesn't need scaling, just to test compression.
double comp_error = kMaxError;
ImageProcessor(
base::BindRepeating([](const SkBitmap& b) { return b; }, small_orig))
.GetJpgImageData(
base::BindOnce(&OutputImageError, &comp_error, small_orig));
test_task_env.RunUntilIdle();
EXPECT_THAT(comp_error, Lt(kMaxError));
// Process the image that needs scaling and compression.
double scale_error = kMaxError;
ImageProcessor(
base::BindRepeating([](const SkBitmap& b) { return b; }, large_orig))
.GetJpgImageData(
base::BindOnce(&OutputImageError, &scale_error, small_orig));
test_task_env.RunUntilIdle();
EXPECT_THAT(scale_error, Lt(kMaxError));
histogram_tester.ExpectBucketCount(metrics_internal::kSourcePixelCount,
max_dim * max_dim /* sample */,
1 /* count */);
histogram_tester.ExpectBucketCount(metrics_internal::kSourcePixelCount,
4 * max_dim * max_dim /* sample */,
1 /* count */);
histogram_tester.ExpectTotalCount(metrics_internal::kSourcePixelCount, 2);
}
} // namespace image_annotation