First version of guetzli sandbox

This commit is contained in:
Bohdan Tyshchenko 2020-08-11 15:48:48 -07:00
parent 258dbcd622
commit d41a3d6d16
20 changed files with 3835 additions and 482 deletions

View File

@ -0,0 +1,2 @@
# Build in C++17 mode without a custom CROSSTOOL
build --cxxopt=-std=c++17

View File

@ -1,9 +1,3 @@
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load(
"@com_google_sandboxed_api//sandboxed_api/bazel:proto.bzl",
"sapi_proto_library",
)
load(
"@com_google_sandboxed_api//sandboxed_api/bazel:sapi.bzl",
"sapi_library",
@ -17,23 +11,17 @@ cc_library(
"@guetzli//:guetzli_lib",
"@com_google_sandboxed_api//sandboxed_api:lenval_core",
"@com_google_sandboxed_api//sandboxed_api:vars",
#"@com_google_sandboxed_api//sandboxed_api/sandbox2/util:temp_file", visibility error
"@png_archive//:png"
],
visibility = ["//visibility:public"]
)
sapi_library(
name = "guetzli_sapi",
#srcs = ["guetzli_transaction.cc"], // Error when try to place definitions insde .cc file
srcs = ["guetzli_transaction.cc"],
hdrs = ["guetzli_sandbox.h", "guetzli_transaction.h"],
functions = [
"ProcessJPEGString",
"ProcessRGBData",
"ButteraugliScoreQuality",
"ReadPng",
"ReadJpegData",
"ReadDataFromFd",
"ProcessJpeg",
"ProcessRgb",
"WriteDataToFd"
],
input_files = ["guetzli_entry_points.h"],
@ -43,23 +31,10 @@ sapi_library(
namespace = "guetzli::sandbox"
)
# cc_library(
# name = "guetzli_sapi_transaction",
# #srcs = ["guetzli_transaction.cc"],
# hdrs = ["guetzli_transaction.h"],
# deps = [
# ":guetzli_sapi"
# ],
# visibility = ["//visibility:public"]
# )
cc_binary(
name="guetzli_sandboxed",
srcs=["guetzli_sandboxed.cc"],
includes = ["."],
visibility= [ "//visibility:public" ],
deps = [
#":guetzli_sapi_transaction"
":guetzli_sapi"
]
)

View File

@ -15,6 +15,7 @@
workspace(name = "guetzli_sandboxed")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
# Include the Sandboxed API dependency if it does not already exist in this
@ -46,22 +47,19 @@ load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
protobuf_deps()
maybe(
git_repository,
local_repository(
name = "butteraugli",
path = "third_party/butteraugli/"
)
http_archive(
name = "guetzli",
remote = "https://github.com/google/guetzli.git",
branch = "master"
build_file = "guetzli.BUILD",
sha256 = "39632357e49db83d9560bf0de560ad833352f36d23b109b0e995b01a37bddb57",
strip_prefix = "guetzli-master",
url = "https://github.com/google/guetzli/archive/master.zip"
)
maybe(
git_repository,
name = "googletest",
remote = "https://github.com/google/googletest",
tag = "release-1.10.0"
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "png_archive",
build_file = "png.BUILD",
@ -76,4 +74,11 @@ http_archive(
sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017",
strip_prefix = "zlib-1.2.10",
url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz",
)
maybe(
git_repository,
name = "googletest",
remote = "https://github.com/google/googletest",
tag = "release-1.10.0"
)

View File

@ -0,0 +1,18 @@
package(default_visibility = ["//visibility:public"])
cc_library(
name = "guetzli_lib",
srcs = glob(
[
"guetzli/*.h",
"guetzli/*.cc",
"guetzli/*.inc",
],
exclude = ["guetzli/guetzli.cc"],
),
copts = [ "-Wno-sign-compare" ],
visibility= [ "//visibility:public" ],
deps = [
"@butteraugli//:butteraugli_lib",
],
)

View File

@ -3,18 +3,25 @@
#include "guetzli_entry_points.h"
#include "png.h"
#include "sandboxed_api/sandbox2/util/fileops.h"
#include "sandboxed_api/util/statusor.h"
#include <algorithm>
#include <iostream>
#include <string>
#include <stdio.h>
#include <sys/stat.h>
#include <vector>
namespace {
inline uint8_t BlendOnBlack(const uint8_t val, const uint8_t alpha) {
return (static_cast<int>(val) * static_cast<int>(alpha) + 128) / 255;
}
constexpr int kBytesPerPixel = 350;
constexpr int kLowestMemusageMB = 100; // in MB
struct GuetzliInitData {
std::string in_data;
guetzli::Params params;
guetzli::ProcessStats stats;
};
template<typename T>
void CopyMemoryToLenVal(const T* data, size_t size,
@ -26,68 +33,63 @@ void CopyMemoryToLenVal(const T* data, size_t size,
out_data->data = new_out;
}
} // namespace
extern "C" bool ProcessJPEGString(const guetzli::Params* params,
int verbose,
sapi::LenValStruct* in_data,
sapi::LenValStruct* out_data)
{
std::string in_data_temp(static_cast<const char*>(in_data->data),
in_data->size);
guetzli::ProcessStats stats;
if (verbose > 0) {
stats.debug_output_file = stderr;
}
std::string temp_out = "";
auto result = guetzli::Process(*params, &stats, in_data_temp, &temp_out);
if (result) {
CopyMemoryToLenVal(temp_out.data(), temp_out.size(), out_data);
}
return result;
}
extern "C" bool ProcessRGBData(const guetzli::Params* params,
int verbose,
sapi::LenValStruct* rgb,
int w, int h,
sapi::LenValStruct* out_data)
{
std::vector<uint8_t> in_data_temp;
in_data_temp.reserve(rgb->size);
auto* rgb_data = static_cast<uint8_t*>(rgb->data);
std::copy(rgb_data, rgb_data + rgb->size, std::back_inserter(in_data_temp));
guetzli::ProcessStats stats;
if (verbose > 0) {
stats.debug_output_file = stderr;
}
std::string temp_out = "";
auto result =
guetzli::Process(*params, &stats, in_data_temp, w, h, &temp_out);
//TODO: Move shared part of the code to another function
if (result) {
CopyMemoryToLenVal(temp_out.data(), temp_out.size(), out_data);
}
return result;
}
extern "C" bool ReadPng(sapi::LenValStruct* in_data,
int* xsize, int* ysize,
sapi::LenValStruct* rgb_out)
{
std::string data(static_cast<const char*>(in_data->data), in_data->size);
std::vector<uint8_t> rgb;
sapi::StatusOr<std::string> ReadFromFd(int fd) {
struct stat file_data;
auto status = fstat(fd, &file_data);
png_structp png_ptr =
if (status < 0) {
return absl::FailedPreconditionError(
"Error reading input from fd"
);
}
auto fsize = file_data.st_size;
std::unique_ptr<char[]> buf(new char[fsize]);
status = read(fd, buf.get(), fsize);
if (status < 0) {
lseek(fd, 0, SEEK_SET);
return absl::FailedPreconditionError(
"Error reading input from fd"
);
}
return std::string(buf.get(), fsize);
}
sapi::StatusOr<GuetzliInitData> PrepareDataForProcessing(
const ProcessingParams* processing_params) {
auto input_status = ReadFromFd(processing_params->remote_fd);
if (!input_status.ok()) {
return input_status.status();
}
guetzli::Params guetzli_params;
guetzli_params.butteraugli_target = static_cast<float>(
guetzli::ButteraugliScoreForQuality(processing_params->quality));
guetzli::ProcessStats stats;
if (processing_params->verbose) {
stats.debug_output_file = stderr;
}
return GuetzliInitData{
std::move(input_status.value()),
guetzli_params,
stats
};
}
inline uint8_t BlendOnBlack(const uint8_t val, const uint8_t alpha) {
return (static_cast<int>(val) * static_cast<int>(alpha) + 128) / 255;
}
bool ReadPNG(const std::string& data, int* xsize, int* ysize,
std::vector<uint8_t>* rgb) {
png_structp png_ptr =
png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!png_ptr) {
return false;
@ -129,7 +131,7 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data,
*xsize = png_get_image_width(png_ptr, info_ptr);
*ysize = png_get_image_height(png_ptr, info_ptr);
rgb.resize(3 * (*xsize) * (*ysize));
rgb->resize(3 * (*xsize) * (*ysize));
const int components = png_get_channels(png_ptr, info_ptr);
switch (components) {
@ -137,7 +139,7 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data,
// GRAYSCALE
for (int y = 0; y < *ysize; ++y) {
const uint8_t* row_in = row_pointers[y];
uint8_t* row_out = &(rgb)[3 * y * (*xsize)];
uint8_t* row_out = &(*rgb)[3 * y * (*xsize)];
for (int x = 0; x < *xsize; ++x) {
const uint8_t gray = row_in[x];
row_out[3 * x + 0] = gray;
@ -151,7 +153,7 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data,
// GRAYSCALE + ALPHA
for (int y = 0; y < *ysize; ++y) {
const uint8_t* row_in = row_pointers[y];
uint8_t* row_out = &(rgb)[3 * y * (*xsize)];
uint8_t* row_out = &(*rgb)[3 * y * (*xsize)];
for (int x = 0; x < *xsize; ++x) {
const uint8_t gray = BlendOnBlack(row_in[2 * x], row_in[2 * x + 1]);
row_out[3 * x + 0] = gray;
@ -165,7 +167,7 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data,
// RGB
for (int y = 0; y < *ysize; ++y) {
const uint8_t* row_in = row_pointers[y];
uint8_t* row_out = &(rgb)[3 * y * (*xsize)];
uint8_t* row_out = &(*rgb)[3 * y * (*xsize)];
memcpy(row_out, row_in, 3 * (*xsize));
}
break;
@ -174,7 +176,7 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data,
// RGBA
for (int y = 0; y < *ysize; ++y) {
const uint8_t* row_in = row_pointers[y];
uint8_t* row_out = &(rgb)[3 * y * (*xsize)];
uint8_t* row_out = &(*rgb)[3 * y * (*xsize)];
for (int x = 0; x < *xsize; ++x) {
const uint8_t alpha = row_in[4 * x + 3];
row_out[3 * x + 0] = BlendOnBlack(row_in[4 * x + 0], alpha);
@ -189,53 +191,84 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data,
return false;
}
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
CopyMemoryToLenVal(rgb.data(), rgb.size(), rgb_out);
return true;
}
extern "C" bool ReadJpegData(sapi::LenValStruct* in_data,
int mode,
int* xsize, int* ysize)
{
std::string data(static_cast<const char*>(in_data->data), in_data->size);
guetzli::JPEGData jpg;
auto result = guetzli::ReadJpeg(data,
static_cast<guetzli::JpegReadMode>(mode), &jpg);
if (result) {
*xsize = jpg.width;
*ysize = jpg.height;
}
return result;
bool CheckMemoryLimitExceeded(int memlimit_mb, int xsize, int ysize) {
double pixels = static_cast<double>(xsize) * ysize;
return memlimit_mb != -1
&& (pixels * kBytesPerPixel / (1 << 20) > memlimit_mb
|| memlimit_mb < kLowestMemusageMB);
}
extern "C" double ButteraugliScoreQuality(double quality) {
return guetzli::ButteraugliScoreForQuality(quality);
} // namespace
extern "C" bool ProcessJpeg(const ProcessingParams* processing_params,
sapi::LenValStruct* output) {
auto processing_data_status = PrepareDataForProcessing(processing_params);
if (!processing_data_status.status().ok()) {
fprintf(stderr, "%s\n", processing_data_status.status().ToString().c_str());
return false;
}
guetzli::JPEGData jpg_header;
if (!guetzli::ReadJpeg(processing_data_status.value().in_data,
guetzli::JPEG_READ_HEADER, &jpg_header)) {
fprintf(stderr, "Error reading JPG data from input file\n");
return false;
}
if (CheckMemoryLimitExceeded(processing_params->memlimit_mb,
jpg_header.width, jpg_header.height)) {
fprintf(stderr, "Memory limit would be exceeded.\n");
return false;
}
std::string out_data;
if (!guetzli::Process(processing_data_status.value().params,
&processing_data_status.value().stats,
processing_data_status.value().in_data,
&out_data)) {
fprintf(stderr, "Guezli processing failed\n");
return false;
}
CopyMemoryToLenVal(out_data.data(), out_data.size(), output);
return true;
}
extern "C" bool ReadDataFromFd(int fd, sapi::LenValStruct* out_data) {
struct stat file_data;
auto status = fstat(fd, &file_data);
if (status < 0) {
extern "C" bool ProcessRgb(const ProcessingParams* processing_params,
sapi::LenValStruct* output) {
auto processing_data_status = PrepareDataForProcessing(processing_params);
if (!processing_data_status.status().ok()) {
fprintf(stderr, "%s\n", processing_data_status.status().ToString().c_str());
return false;
}
auto fsize = file_data.st_size;
std::unique_ptr<char[]> buf(new char[fsize]);
status = read(fd, buf.get(), fsize);
int xsize, ysize;
std::vector<uint8_t> rgb;
if (status < 0) {
if (!ReadPNG(processing_data_status.value().in_data, &xsize, &ysize, &rgb)) {
fprintf(stderr, "Error reading PNG data from input file\n");
return false;
}
CopyMemoryToLenVal(buf.get(), fsize, out_data);
if (CheckMemoryLimitExceeded(processing_params->memlimit_mb, xsize, ysize)) {
fprintf(stderr, "Memory limit would be exceeded.\n");
return false;
}
std::string out_data;
if (!guetzli::Process(processing_data_status.value().params,
&processing_data_status.value().stats,
rgb, xsize, ysize, &out_data)) {
fprintf(stderr, "Guetzli processing failed\n");
return false;
}
CopyMemoryToLenVal(out_data.data(), out_data.size(), output);
return true;
}

View File

@ -4,26 +4,15 @@
#include "sandboxed_api/lenval_core.h"
#include "sandboxed_api/vars.h"
extern "C" bool ProcessJPEGString(const guetzli::Params* params,
int verbose,
sapi::LenValStruct* in_data,
sapi::LenValStruct* out_data);
extern "C" bool ProcessRGBData(const guetzli::Params* params,
int verbose,
sapi::LenValStruct* rgb,
int w, int h,
sapi::LenValStruct* out_data);
extern "C" bool ReadPng(sapi::LenValStruct* in_data,
int* xsize, int* ysize,
sapi::LenValStruct* rgb_out);
extern "C" bool ReadJpegData(sapi::LenValStruct* in_data,
int mode, int* xsize, int* ysize);
extern "C" double ButteraugliScoreQuality(double quality);
extern "C" bool ReadDataFromFd(int fd, sapi::LenValStruct* out_data);
struct ProcessingParams {
int remote_fd = -1;
int verbose = 0;
int quality = 0;
int memlimit_mb = 0;
};
extern "C" bool ProcessJpeg(const ProcessingParams* processing_params,
sapi::LenValStruct* output);
extern "C" bool ProcessRgb(const ProcessingParams* processing_params,
sapi::LenValStruct* output);
extern "C" bool WriteDataToFd(int fd, sapi::LenValStruct* data);

View File

@ -14,17 +14,6 @@ namespace {
constexpr int kDefaultJPEGQuality = 95;
constexpr int kDefaultMemlimitMB = 6000; // in MB
//constexpr absl::string_view kMktempSuffix = "XXXXXX";
// sapi::StatusOr<std::pair<std::string, int>> CreateNamedTempFile(
// absl::string_view prefix) {
// std::string name_template = absl::StrCat(prefix, kMktempSuffix);
// int fd = mkstemp(&name_template[0]);
// if (fd < 0) {
// return absl::UnknownError("Error creating temp file");
// }
// return std::pair<std::string, int>{std::move(name_template), fd};
// }
void TerminateHandler() {
fprintf(stderr, "Unhandled exception. Most likely insufficient memory available.\n"
@ -96,21 +85,6 @@ int main(int argc, const char** argv) {
return 1;
}
// auto out_temp_file = CreateNamedTempFile("/tmp/" + std::string(argv[opt_idx + 1]));
// if (!out_temp_file.ok()) {
// fprintf(stderr, "Can't create temporary output file: %s\n",
// argv[opt_idx + 1]);
// return 1;
// }
// sandbox2::file_util::fileops::FDCloser out_fd_closer(
// out_temp_file.value().second);
// if (unlink(out_temp_file.value().first.c_str()) < 0) {
// fprintf(stderr, "Error unlinking temp out file: %s\n",
// out_temp_file.value().first.c_str());
// return 1;
// }
sandbox2::file_util::fileops::FDCloser out_fd_closer(
open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR));
@ -119,14 +93,6 @@ int main(int argc, const char** argv) {
return 1;
}
// sandbox2::file_util::fileops::FDCloser out_fd_closer(open(argv[opt_idx + 1],
// O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP));
// if (out_fd_closer.get() < 0) {
// fprintf(stderr, "Can't open output file: %s\n", argv[opt_idx + 1]);
// return 1;
// }
guetzli::sandbox::TransactionParams params = {
in_fd_closer.get(),
out_fd_closer.get(),
@ -143,13 +109,19 @@ int main(int argc, const char** argv) {
if (remove(argv[opt_idx + 1]) < 0) {
fprintf(stderr, "Error deleting existing output file: %s\n",
argv[opt_idx + 1]);
return 1;
}
}
std::stringstream path;
path << "/proc/self/fd/" << out_fd_closer.get();
linkat(AT_FDCWD, path.str().c_str(), AT_FDCWD, argv[opt_idx + 1],
AT_SYMLINK_FOLLOW);
if (linkat(AT_FDCWD, path.str().c_str(), AT_FDCWD, argv[opt_idx + 1],
AT_SYMLINK_FOLLOW) < 0) {
fprintf(stderr, "Error linking %s\n",
argv[opt_idx + 1]);
return 1;
}
}
else {
fprintf(stderr, "%s\n", result.ToString().c_str()); // Use cerr instead ?

View File

@ -1,11 +1,38 @@
#include "guetzli_transaction.h"
#include <iostream>
#include <memory>
namespace guetzli {
namespace sandbox {
absl::Status GuetzliTransaction::Init() {
// Close remote fd if transaction is repeated
if (in_fd_.GetRemoteFd() != -1) {
SAPI_RETURN_IF_ERROR(in_fd_.CloseRemoteFd(sandbox()->GetRpcChannel()));
}
if (out_fd_.GetRemoteFd() != -1) {
SAPI_RETURN_IF_ERROR(out_fd_.CloseRemoteFd(sandbox()->GetRpcChannel()));
}
// Reposition back to the beginning of file
if (lseek(in_fd_.GetValue(), 0, SEEK_CUR) != 0) {
if (lseek(in_fd_.GetValue(), 0, SEEK_SET) != 0) {
return absl::FailedPreconditionError(
"Error returnig cursor to the beginning"
);
}
}
// Choosing between jpg and png modes
sapi::StatusOr<ImageType> image_type = GetImageTypeFromFd(in_fd_.GetValue());
if (!image_type.ok()) {
return image_type.status();
}
image_type_ = image_type.value();
SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&in_fd_));
SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&out_fd_));
@ -18,125 +45,36 @@ absl::Status GuetzliTransaction::Init() {
"Error receiving remote FD: remote output fd is set to -1");
}
in_fd_.OwnLocalFd(false); // FDCloser will close local fd
out_fd_.OwnLocalFd(false); // FDCloser will close local fd
return absl::OkStatus();
}
absl::Status GuetzliTransaction::ProcessPng(GuetzliAPi* api,
sapi::v::Struct<Params>* params,
sapi::v::LenVal* input,
sapi::v::LenVal* output) const {
sapi::v::Int xsize;
sapi::v::Int ysize;
sapi::v::LenVal rgb_in(0);
auto read_result = api->ReadPng(input->PtrBefore(), xsize.PtrBoth(),
ysize.PtrBoth(), rgb_in.PtrBoth());
if (!read_result.value_or(false)) {
return absl::FailedPreconditionError(
"Error reading PNG data from input file"
);
}
double pixels = static_cast<double>(xsize.GetValue()) * ysize.GetValue();
if (params_.memlimit_mb != -1
&& (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb
|| params_.memlimit_mb < kLowestMemusageMB)) {
return absl::FailedPreconditionError(
"Memory limit would be exceeded"
);
}
auto result = api->ProcessRGBData(params->PtrBefore(), params_.verbose,
rgb_in.PtrBefore(), xsize.GetValue(),
ysize.GetValue(), output->PtrBoth());
if (!result.value_or(false)) {
return absl::FailedPreconditionError(
"Guetzli processing failed"
);
}
return absl::OkStatus();
}
absl::Status GuetzliTransaction::ProcessJpeg(GuetzliApi* api,
sapi::v::Struct<Params>* params,
sapi::v::LenVal* input,
sapi::v::LenVal* output) const {
::sapi::v::Int xsize;
::sapi::v::Int ysize;
auto read_result = api->ReadJpegData(input->PtrBefore(), 0, xsize.PtrBoth(),
ysize.PtrBoth());
if (!read_result.value_or(false)) {
return absl::FailedPreconditionError(
"Error reading JPG data from input file"
);
}
double pixels = static_cast<double>(xsize.GetValue()) * ysize.GetValue();
if (params_.memlimit_mb != -1
&& (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb
|| params_.memlimit_mb < kLowestMemusageMB)) {
return absl::FailedPreconditionError(
"Memory limit would be exceeded"
);
}
auto result = api->ProcessJPEGString(params->PtrBefore(), params_.verbose,
input->PtrBefore(), output->PtrBoth());
if (!result.value_or(false)) {
return absl::FailedPreconditionError(
"Guetzli processing failed"
);
}
return absl::OkStatus();
}
absl::Status GuetzliTransaction::Main() {
GuetzliApi api(sandbox());
sapi::v::LenVal input(0);
sapi::v::LenVal output(0);
sapi::v::Struct<Params> params;
auto read_result = api.ReadDataFromFd(in_fd_.GetRemoteFd(), input.PtrBoth());
if (!read_result.value_or(false)) {
return absl::FailedPreconditionError(
"Error reading data inside sandbox"
);
}
auto score_quality_result = api.ButteraugliScoreQuality(params_.quality);
if (!score_quality_result.ok()) {
return absl::FailedPreconditionError(
"Error calculating butteraugli score"
);
}
params.mutable_data()->butteraugli_target = score_quality_result.value();
static const unsigned char kPNGMagicBytes[] = {
0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n',
sapi::v::Struct<ProcessingParams> processing_params;
*processing_params.mutable_data() = {in_fd_.GetRemoteFd(),
params_.verbose,
params_.quality,
params_.memlimit_mb
};
auto result_status = image_type_ == ImageType::JPEG ?
api.ProcessJpeg(processing_params.PtrBefore(), output.PtrBoth()) :
api.ProcessRgb(processing_params.PtrBefore(), output.PtrBoth());
if (input.GetDataSize() >= 8 &&
memcmp(input.GetData(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) {
auto process_status = ProcessPng(&api, &params, &input, &output);
if (!process_status.ok()) {
return process_status;
}
} else {
auto process_status = ProcessJpeg(&api, &params, &input, &output);
if (!process_status.ok()) {
return process_status;
}
if (!result_status.value_or(false)) {
std::stringstream error_stream;
error_stream << "Error processing "
<< (image_type_ == ImageType::JPEG ? "jpeg" : "rgb") << " data"
<< std::endl;
return absl::FailedPreconditionError(
error_stream.str()
);
}
auto write_result = api.WriteDataToFd(out_fd_.GetRemoteFd(),
@ -156,5 +94,27 @@ time_t GuetzliTransaction::CalculateTimeLimitFromImageSize(
return (pixels / kMpixPixels + 5) * 60;
}
sapi::StatusOr<ImageType> GuetzliTransaction::GetImageTypeFromFd(int fd) const {
static const unsigned char kPNGMagicBytes[] = {
0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n',
};
char read_buf[8];
if (read(fd, read_buf, 8) != 8) {
return absl::FailedPreconditionError(
"Error determining type of the input file"
);
}
if (lseek(fd, 0, SEEK_SET) != 0) {
return absl::FailedPreconditionError(
"Error returnig cursor to the beginning"
);
}
return memcmp(read_buf, kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0 ?
ImageType::PNG : ImageType::JPEG;
}
} // namespace sandbox
} // namespace guetzli

View File

@ -10,22 +10,24 @@
namespace guetzli {
namespace sandbox {
constexpr int kDefaultTransactionRetryCount = 1;
constexpr int kDefaultTransactionRetryCount = 0;
constexpr uint64_t kMpixPixels = 1'000'000;
constexpr int kBytesPerPixel = 350;
constexpr int kLowestMemusageMB = 100; // in MB
struct TransactionParams {
int in_fd;
int out_fd;
int verbose;
int quality;
int memlimit_mb;
enum class ImageType {
JPEG,
PNG
};
//Add optional time limit/retry count as a constructors arguments
//Use differenet status errors
struct TransactionParams {
int in_fd = -1;
int out_fd = -1;
int verbose = 0;
int quality = 0;
int memlimit_mb = 0;
};
// Instance of this transaction shouldn't be reused
// Create a new one for each processing operation
class GuetzliTransaction : public sapi::Transaction {
public:
GuetzliTransaction(TransactionParams&& params)
@ -34,7 +36,9 @@ class GuetzliTransaction : public sapi::Transaction {
, in_fd_(params_.in_fd)
, out_fd_(params_.out_fd)
{
//TODO: Add retry count as a parameter
sapi::Transaction::set_retry_count(kDefaultTransactionRetryCount);
//TODO: Try to use sandbox().set_wall_limit instead of infinite time limit
sapi::Transaction::SetTimeLimit(0);
}
@ -42,15 +46,7 @@ class GuetzliTransaction : public sapi::Transaction {
absl::Status Init() override;
absl::Status Main() final;
absl::Status ProcessPng(GuetzliApi* api,
sapi::v::Struct<Params>* params,
sapi::v::LenVal* input,
sapi::v::LenVal* output) const;
absl::Status ProcessJpeg(GuetzliApi* api,
sapi::v::Struct<Params>* params,
sapi::v::LenVal* input,
sapi::v::LenVal* output) const;
sapi::StatusOr<ImageType> GetImageTypeFromFd(int fd) const;
// As guetzli takes roughly 1 minute of CPU per 1 MPix we need to calculate
// approximate time for transaction to complete
@ -59,158 +55,8 @@ class GuetzliTransaction : public sapi::Transaction {
const TransactionParams params_;
sapi::v::Fd in_fd_;
sapi::v::Fd out_fd_;
ImageType image_type_ = ImageType::JPEG;
};
absl::Status GuetzliTransaction::Init() {
SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&in_fd_));
SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&out_fd_));
if (in_fd_.GetRemoteFd() < 0) {
return absl::FailedPreconditionError(
"Error receiving remote FD: remote input fd is set to -1");
}
if (out_fd_.GetRemoteFd() < 0) {
return absl::FailedPreconditionError(
"Error receiving remote FD: remote output fd is set to -1");
}
return absl::OkStatus();
}
absl::Status GuetzliTransaction::ProcessPng(GuetzliApi* api,
sapi::v::Struct<Params>* params,
sapi::v::LenVal* input,
sapi::v::LenVal* output) const {
sapi::v::Int xsize;
sapi::v::Int ysize;
sapi::v::LenVal rgb_in(0);
auto read_result = api->ReadPng(input->PtrBefore(), xsize.PtrBoth(),
ysize.PtrBoth(), rgb_in.PtrBoth());
if (!read_result.value_or(false)) {
return absl::FailedPreconditionError(
"Error reading PNG data from input file"
);
}
double pixels = static_cast<double>(xsize.GetValue()) * ysize.GetValue();
if (params_.memlimit_mb != -1
&& (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb
|| params_.memlimit_mb < kLowestMemusageMB)) {
return absl::FailedPreconditionError(
"Memory limit would be exceeded"
);
}
auto result = api->ProcessRGBData(params->PtrBefore(), params_.verbose,
rgb_in.PtrBefore(), xsize.GetValue(),
ysize.GetValue(), output->PtrBoth());
if (!result.value_or(false)) {
return absl::FailedPreconditionError(
"Guetzli processing failed"
);
}
return absl::OkStatus();
}
absl::Status GuetzliTransaction::ProcessJpeg(GuetzliApi* api,
sapi::v::Struct<Params>* params,
sapi::v::LenVal* input,
sapi::v::LenVal* output) const {
sapi::v::Int xsize;
sapi::v::Int ysize;
auto read_result = api->ReadJpegData(input->PtrBefore(), 0, xsize.PtrBoth(),
ysize.PtrBoth());
if (!read_result.value_or(false)) {
return absl::FailedPreconditionError(
"Error reading JPG data from input file"
);
}
double pixels = static_cast<double>(xsize.GetValue()) * ysize.GetValue();
if (params_.memlimit_mb != -1
&& (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb
|| params_.memlimit_mb < kLowestMemusageMB)) {
return absl::FailedPreconditionError(
"Memory limit would be exceeded"
);
}
auto result = api->ProcessJPEGString(params->PtrBefore(), params_.verbose,
input->PtrBefore(), output->PtrBoth());
if (!result.value_or(false)) {
return absl::FailedPreconditionError(
"Guetzli processing failed"
);
}
return absl::OkStatus();
}
absl::Status GuetzliTransaction::Main() {
GuetzliApi api(sandbox());
sapi::v::LenVal input(0);
sapi::v::LenVal output(0);
sapi::v::Struct<Params> params;
auto read_result = api.ReadDataFromFd(in_fd_.GetRemoteFd(), input.PtrBoth());
if (!read_result.value_or(false)) {
return absl::FailedPreconditionError(
"Error reading data inside sandbox"
);
}
auto score_quality_result = api.ButteraugliScoreQuality(params_.quality);
if (!score_quality_result.ok()) {
return absl::FailedPreconditionError(
"Error calculating butteraugli score"
);
}
params.mutable_data()->butteraugli_target = score_quality_result.value();
static const unsigned char kPNGMagicBytes[] = {
0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n',
};
if (input.GetDataSize() >= 8 &&
memcmp(input.GetData(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) {
auto process_status = ProcessPng(&api, &params, &input, &output);
if (!process_status.ok()) {
return process_status;
}
} else {
auto process_status = ProcessJpeg(&api, &params, &input, &output);
if (!process_status.ok()) {
return process_status;
}
}
auto write_result = api.WriteDataToFd(out_fd_.GetRemoteFd(),
output.PtrBefore());
if (!write_result.value_or(false)) {
return absl::FailedPreconditionError(
"Error writing file inside sandbox"
);
}
return absl::OkStatus();
}
time_t GuetzliTransaction::CalculateTimeLimitFromImageSize(
uint64_t pixels) const {
return (pixels / kMpixPixels + 5) * 60;
}
} // namespace sandbox
} // namespace guetzli

View File

@ -0,0 +1,24 @@
cc_library(
name = "butteraugli_lib",
srcs = [
"butteraugli/butteraugli.cc",
"butteraugli/butteraugli.h",
],
hdrs = [
"butteraugli/butteraugli.h",
],
copts = ["-Wno-sign-compare"],
visibility = ["//visibility:public"],
)
cc_binary(
name = "butteraugli",
srcs = ["butteraugli/butteraugli_main.cc"],
copts = ["-Wno-sign-compare"],
visibility = ["//visibility:public"],
deps = [
":butteraugli_lib",
"@jpeg_archive//:jpeg",
"@png_archive//:png",
],
)

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,68 @@
# butteraugli
> A tool for measuring perceived differences between images
## Introduction
Butteraugli is a project that estimates the psychovisual similarity of two
images. It gives a score for the images that is reliable in the domain of barely
noticeable differences. Butteraugli not only gives a scalar score, but also
computes a spatial map of the level of differences.
One of the main motivations for this project is the statistical differences in
location and density of different color receptors, particularly the low density
of blue cones in the fovea. Another motivation comes from more accurate modeling
of ganglion cells, particularly the frequency space inhibition.
## Use
Butteraugli can work as a quality metric for lossy image and video compression.
On our small test corpus butteraugli performs better than our implementations of
the reference methods, psnrhsv-m, ssim, and our yuv-color-space variant of ssim.
One possible use is to define the quality level setting used in a jpeg
compressor, or to compare two or more compression methods at the same level of
psychovisual differences.
Butteraugli is intended to be a research tool more than a practical tool for
choosing compression formats. We don't know how well butteraugli performs with
major deformations -- we have mostly tuned it within a small range of quality,
roughly corresponding to jpeg qualities 90 to 95.
## Interface
Only a C++ interface is provided. The interface takes two images and outputs a
map together with a scalar value defining the difference. The scalar value can
be compared to two reference values that divide the value space into three
experience classes: 'great', 'acceptable' and 'not acceptable'.
## Build instructions
Install [Bazel](http://bazel.build) by following the
[instructions](https://www.bazel.build/docs/install.html). Run `bazel build -c opt
//:butteraugli` in the directory that contains this README file to build the
[command-line utility](#cmdline-tool). If you want to use Butteraugli as a
library, depend on the `//:butteraugli_lib` target.
Alternatively, you can use the Makefile provided in the `butteraugli` directory,
after ensuring that [libpng](http://www.libpng.org/) and
[libjpeg](http://ijg.org/) are installed. On some systems you might need to also
install corresponding `-dev` packages.
The code is portable and also compiles on Windows after defining
`_CRT_SECURE_NO_WARNINGS` in the project settings.
## Command-line utility {#cmdline-tool}
Butteraugli, apart from the library, comes bundled with a comparison tool. The
comparison tool supports PNG and JPG images as inputs. To compare images, run:
```
butteraugli image1.{png|jpg} image2.{png|jpg}
```
The tool can also produce a heatmap of differences between images. The heatmap
will be output as a PNM image. To produce one, run:
```
butteraugli image1.{png|jpg} image2.{png|jpg} heatmap.pnm
```

View File

@ -0,0 +1,25 @@
workspace(name = "butteraugli")
new_http_archive(
name = "png_archive",
url = "http://github.com/glennrp/libpng/archive/v1.2.57.zip",
sha256 = "a941dc09ca00148fe7aaf4ecdd6a67579c293678ed1e1cf633b5ffc02f4f8cf7",
strip_prefix = "libpng-1.2.57",
build_file = "png.BUILD",
)
new_http_archive(
name = "zlib_archive",
url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz",
sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017",
strip_prefix = "zlib-1.2.10",
build_file = "zlib.BUILD",
)
new_http_archive(
name = "jpeg_archive",
url = "http://www.ijg.org/files/jpegsrc.v9b.tar.gz",
sha256 = "240fd398da741669bf3c90366f58452ea59041cacc741a489b99f2f6a0bad052",
strip_prefix = "jpeg-9b",
build_file = "jpeg.BUILD",
)

View File

@ -0,0 +1,7 @@
LDLIBS += -lpng -ljpeg
CXXFLAGS += -std=c++11 -I..
LINK.o = $(LINK.cc)
all: butteraugli.o butteraugli_main.o butteraugli
butteraugli: butteraugli.o butteraugli_main.o

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,619 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Disclaimer: This is not an official Google product.
//
// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com)
#ifndef BUTTERAUGLI_BUTTERAUGLI_H_
#define BUTTERAUGLI_BUTTERAUGLI_H_
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <vector>
#define BUTTERAUGLI_ENABLE_CHECKS 0
// This is the main interface to butteraugli image similarity
// analysis function.
namespace butteraugli {
template<typename T>
class Image;
using Image8 = Image<uint8_t>;
using ImageF = Image<float>;
// ButteraugliInterface defines the public interface for butteraugli.
//
// It calculates the difference between rgb0 and rgb1.
//
// rgb0 and rgb1 contain the images. rgb0[c][px] and rgb1[c][px] contains
// the red image for c == 0, green for c == 1, blue for c == 2. Location index
// px is calculated as y * xsize + x.
//
// Value of pixels of images rgb0 and rgb1 need to be represented as raw
// intensity. Most image formats store gamma corrected intensity in pixel
// values. This gamma correction has to be removed, by applying the following
// function:
// butteraugli_val = 255.0 * pow(png_val / 255.0, gamma);
// A typical value of gamma is 2.2. It is usually stored in the image header.
// Take care not to confuse that value with its inverse. The gamma value should
// be always greater than one.
// Butteraugli does not work as intended if the caller does not perform
// gamma correction.
//
// diffmap will contain an image of the size xsize * ysize, containing
// localized differences for values px (indexed with the px the same as rgb0
// and rgb1). diffvalue will give a global score of similarity.
//
// A diffvalue smaller than kButteraugliGood indicates that images can be
// observed as the same image.
// diffvalue larger than kButteraugliBad indicates that a difference between
// the images can be observed.
// A diffvalue between kButteraugliGood and kButteraugliBad indicates that
// a subtle difference can be observed between the images.
//
// Returns true on success.
bool ButteraugliInterface(const std::vector<ImageF> &rgb0,
const std::vector<ImageF> &rgb1,
ImageF &diffmap,
double &diffvalue);
const double kButteraugliQuantLow = 0.26;
const double kButteraugliQuantHigh = 1.454;
// Converts the butteraugli score into fuzzy class values that are continuous
// at the class boundary. The class boundary location is based on human
// raters, but the slope is arbitrary. Particularly, it does not reflect
// the expectation value of probabilities of the human raters. It is just
// expected that a smoother class boundary will allow for higher-level
// optimization algorithms to work faster.
//
// Returns 2.0 for a perfect match, and 1.0 for 'ok', 0.0 for bad. Because the
// scoring is fuzzy, a butteraugli score of 0.96 would return a class of
// around 1.9.
double ButteraugliFuzzyClass(double score);
// Input values should be in range 0 (bad) to 2 (good). Use
// kButteraugliNormalization as normalization.
double ButteraugliFuzzyInverse(double seek);
// Returns a map which can be used for adaptive quantization. Values can
// typically range from kButteraugliQuantLow to kButteraugliQuantHigh. Low
// values require coarse quantization (e.g. near random noise), high values
// require fine quantization (e.g. in smooth bright areas).
bool ButteraugliAdaptiveQuantization(size_t xsize, size_t ysize,
const std::vector<std::vector<float> > &rgb, std::vector<float> &quant);
// Implementation details, don't use anything below or your code will
// break in the future.
#ifdef _MSC_VER
#define BUTTERAUGLI_RESTRICT __restrict
#else
#define BUTTERAUGLI_RESTRICT __restrict__
#endif
#ifdef _MSC_VER
#define BUTTERAUGLI_INLINE __forceinline
#else
#define BUTTERAUGLI_INLINE inline
#endif
#ifdef __clang__
// Early versions of Clang did not support __builtin_assume_aligned.
#define BUTTERAUGLI_HAS_ASSUME_ALIGNED __has_builtin(__builtin_assume_aligned)
#elif defined(__GNUC__)
#define BUTTERAUGLI_HAS_ASSUME_ALIGNED 1
#else
#define BUTTERAUGLI_HAS_ASSUME_ALIGNED 0
#endif
// Returns a void* pointer which the compiler then assumes is N-byte aligned.
// Example: float* PIK_RESTRICT aligned = (float*)PIK_ASSUME_ALIGNED(in, 32);
//
// The assignment semantics are required by GCC/Clang. ICC provides an in-place
// __assume_aligned, whereas MSVC's __assume appears unsuitable.
#if BUTTERAUGLI_HAS_ASSUME_ALIGNED
#define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) __builtin_assume_aligned((ptr), (align))
#else
#define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) (ptr)
#endif // BUTTERAUGLI_HAS_ASSUME_ALIGNED
// Functions that depend on the cache line size.
class CacheAligned {
public:
static constexpr size_t kPointerSize = sizeof(void *);
static constexpr size_t kCacheLineSize = 64;
// The aligned-return annotation is only allowed on function declarations.
static void *Allocate(const size_t bytes);
static void Free(void *aligned_pointer);
};
template <typename T>
using CacheAlignedUniquePtrT = std::unique_ptr<T[], void (*)(void *)>;
using CacheAlignedUniquePtr = CacheAlignedUniquePtrT<uint8_t>;
template <typename T = uint8_t>
static inline CacheAlignedUniquePtrT<T> Allocate(const size_t entries) {
return CacheAlignedUniquePtrT<T>(
static_cast<T * const BUTTERAUGLI_RESTRICT>(
CacheAligned::Allocate(entries * sizeof(T))),
CacheAligned::Free);
}
// Returns the smallest integer not less than "amount" that is divisible by
// "multiple", which must be a power of two.
template <size_t multiple>
static inline size_t Align(const size_t amount) {
static_assert(multiple != 0 && ((multiple & (multiple - 1)) == 0),
"Align<> argument must be a power of two");
return (amount + multiple - 1) & ~(multiple - 1);
}
// Single channel, contiguous (cache-aligned) rows separated by padding.
// T must be POD.
//
// Rationale: vectorization benefits from aligned operands - unaligned loads and
// especially stores are expensive when the address crosses cache line
// boundaries. Introducing padding after each row ensures the start of a row is
// aligned, and that row loops can process entire vectors (writes to the padding
// are allowed and ignored).
//
// We prefer a planar representation, where channels are stored as separate
// 2D arrays, because that simplifies vectorization (repeating the same
// operation on multiple adjacent components) without the complexity of a
// hybrid layout (8 R, 8 G, 8 B, ...). In particular, clients can easily iterate
// over all components in a row and Image requires no knowledge of the pixel
// format beyond the component type "T". The downside is that we duplicate the
// xsize/ysize members for each channel.
//
// This image layout could also be achieved with a vector and a row accessor
// function, but a class wrapper with support for "deleter" allows wrapping
// existing memory allocated by clients without copying the pixels. It also
// provides convenient accessors for xsize/ysize, which shortens function
// argument lists. Supports move-construction so it can be stored in containers.
template <typename ComponentType>
class Image {
// Returns cache-aligned row stride, being careful to avoid 2K aliasing.
static size_t BytesPerRow(const size_t xsize) {
// Allow reading one extra AVX-2 vector on the right margin.
const size_t row_size = xsize * sizeof(T) + 32;
const size_t align = CacheAligned::kCacheLineSize;
size_t bytes_per_row = (row_size + align - 1) & ~(align - 1);
// During the lengthy window before writes are committed to memory, CPUs
// guard against read after write hazards by checking the address, but
// only the lower 11 bits. We avoid a false dependency between writes to
// consecutive rows by ensuring their sizes are not multiples of 2 KiB.
if (bytes_per_row % 2048 == 0) {
bytes_per_row += align;
}
return bytes_per_row;
}
public:
using T = ComponentType;
Image() : xsize_(0), ysize_(0), bytes_per_row_(0),
bytes_(static_cast<uint8_t*>(nullptr), Ignore) {}
Image(const size_t xsize, const size_t ysize)
: xsize_(xsize),
ysize_(ysize),
bytes_per_row_(BytesPerRow(xsize)),
bytes_(Allocate(bytes_per_row_ * ysize)) {}
Image(const size_t xsize, const size_t ysize, T val)
: xsize_(xsize),
ysize_(ysize),
bytes_per_row_(BytesPerRow(xsize)),
bytes_(Allocate(bytes_per_row_ * ysize)) {
for (size_t y = 0; y < ysize_; ++y) {
T* const BUTTERAUGLI_RESTRICT row = Row(y);
for (int x = 0; x < xsize_; ++x) {
row[x] = val;
}
}
}
Image(const size_t xsize, const size_t ysize,
uint8_t * const BUTTERAUGLI_RESTRICT bytes,
const size_t bytes_per_row)
: xsize_(xsize),
ysize_(ysize),
bytes_per_row_(bytes_per_row),
bytes_(bytes, Ignore) {}
// Move constructor (required for returning Image from function)
Image(Image &&other)
: xsize_(other.xsize_),
ysize_(other.ysize_),
bytes_per_row_(other.bytes_per_row_),
bytes_(std::move(other.bytes_)) {}
// Move assignment (required for std::vector)
Image &operator=(Image &&other) {
xsize_ = other.xsize_;
ysize_ = other.ysize_;
bytes_per_row_ = other.bytes_per_row_;
bytes_ = std::move(other.bytes_);
return *this;
}
void Swap(Image &other) {
std::swap(xsize_, other.xsize_);
std::swap(ysize_, other.ysize_);
std::swap(bytes_per_row_, other.bytes_per_row_);
std::swap(bytes_, other.bytes_);
}
// How many pixels.
size_t xsize() const { return xsize_; }
size_t ysize() const { return ysize_; }
T *const BUTTERAUGLI_RESTRICT Row(const size_t y) {
#ifdef BUTTERAUGLI_ENABLE_CHECKS
if (y >= ysize_) {
printf("Row %zu out of bounds (ysize=%zu)\n", y, ysize_);
abort();
}
#endif
void *row = bytes_.get() + y * bytes_per_row_;
return reinterpret_cast<T *>(BUTTERAUGLI_ASSUME_ALIGNED(row, 64));
}
const T *const BUTTERAUGLI_RESTRICT Row(const size_t y) const {
#ifdef BUTTERAUGLI_ENABLE_CHECKS
if (y >= ysize_) {
printf("Const row %zu out of bounds (ysize=%zu)\n", y, ysize_);
abort();
}
#endif
void *row = bytes_.get() + y * bytes_per_row_;
return reinterpret_cast<const T *>(BUTTERAUGLI_ASSUME_ALIGNED(row, 64));
}
// Raw access to byte contents, for interfacing with other libraries.
// Unsigned char instead of char to avoid surprises (sign extension).
uint8_t * const BUTTERAUGLI_RESTRICT bytes() { return bytes_.get(); }
const uint8_t * const BUTTERAUGLI_RESTRICT bytes() const {
return bytes_.get();
}
size_t bytes_per_row() const { return bytes_per_row_; }
// Returns number of pixels (some of which are padding) per row. Useful for
// computing other rows via pointer arithmetic.
intptr_t PixelsPerRow() const {
static_assert(CacheAligned::kCacheLineSize % sizeof(T) == 0,
"Padding must be divisible by the pixel size.");
return static_cast<intptr_t>(bytes_per_row_ / sizeof(T));
}
private:
// Deleter used when bytes are not owned.
static void Ignore(void *ptr) {}
// (Members are non-const to enable assignment during move-assignment.)
size_t xsize_; // original intended pixels, not including any padding.
size_t ysize_;
size_t bytes_per_row_; // [bytes] including padding.
CacheAlignedUniquePtr bytes_;
};
// Returns newly allocated planes of the given dimensions.
template <typename T>
static inline std::vector<Image<T>> CreatePlanes(const size_t xsize,
const size_t ysize,
const size_t num_planes) {
std::vector<Image<T>> planes;
planes.reserve(num_planes);
for (size_t i = 0; i < num_planes; ++i) {
planes.emplace_back(xsize, ysize);
}
return planes;
}
// Returns a new image with the same dimensions and pixel values.
template <typename T>
static inline Image<T> CopyPixels(const Image<T> &other) {
Image<T> copy(other.xsize(), other.ysize());
const void *BUTTERAUGLI_RESTRICT from = other.bytes();
void *BUTTERAUGLI_RESTRICT to = copy.bytes();
memcpy(to, from, other.ysize() * other.bytes_per_row());
return copy;
}
// Returns new planes with the same dimensions and pixel values.
template <typename T>
static inline std::vector<Image<T>> CopyPlanes(
const std::vector<Image<T>> &planes) {
std::vector<Image<T>> copy;
copy.reserve(planes.size());
for (const Image<T> &plane : planes) {
copy.push_back(CopyPixels(plane));
}
return copy;
}
// Compacts a padded image into a preallocated packed vector.
template <typename T>
static inline void CopyToPacked(const Image<T> &from, std::vector<T> *to) {
const size_t xsize = from.xsize();
const size_t ysize = from.ysize();
#if BUTTERAUGLI_ENABLE_CHECKS
if (to->size() < xsize * ysize) {
printf("%zu x %zu exceeds %zu capacity\n", xsize, ysize, to->size());
abort();
}
#endif
for (size_t y = 0; y < ysize; ++y) {
const float* const BUTTERAUGLI_RESTRICT row_from = from.Row(y);
float* const BUTTERAUGLI_RESTRICT row_to = to->data() + y * xsize;
memcpy(row_to, row_from, xsize * sizeof(T));
}
}
// Expands a packed vector into a preallocated padded image.
template <typename T>
static inline void CopyFromPacked(const std::vector<T> &from, Image<T> *to) {
const size_t xsize = to->xsize();
const size_t ysize = to->ysize();
assert(from.size() == xsize * ysize);
for (size_t y = 0; y < ysize; ++y) {
const float* const BUTTERAUGLI_RESTRICT row_from =
from.data() + y * xsize;
float* const BUTTERAUGLI_RESTRICT row_to = to->Row(y);
memcpy(row_to, row_from, xsize * sizeof(T));
}
}
template <typename T>
static inline std::vector<Image<T>> PlanesFromPacked(
const size_t xsize, const size_t ysize,
const std::vector<std::vector<T>> &packed) {
std::vector<Image<T>> planes;
planes.reserve(packed.size());
for (const std::vector<T> &p : packed) {
planes.push_back(Image<T>(xsize, ysize));
CopyFromPacked(p, &planes.back());
}
return planes;
}
template <typename T>
static inline std::vector<std::vector<T>> PackedFromPlanes(
const std::vector<Image<T>> &planes) {
assert(!planes.empty());
const size_t num_pixels = planes[0].xsize() * planes[0].ysize();
std::vector<std::vector<T>> packed;
packed.reserve(planes.size());
for (const Image<T> &image : planes) {
packed.push_back(std::vector<T>(num_pixels));
CopyToPacked(image, &packed.back());
}
return packed;
}
struct PsychoImage {
std::vector<ImageF> uhf;
std::vector<ImageF> hf;
std::vector<ImageF> mf;
std::vector<ImageF> lf;
};
class ButteraugliComparator {
public:
ButteraugliComparator(const std::vector<ImageF>& rgb0);
// Computes the butteraugli map between the original image given in the
// constructor and the distorted image give here.
void Diffmap(const std::vector<ImageF>& rgb1, ImageF& result) const;
// Same as above, but OpsinDynamicsImage() was already applied.
void DiffmapOpsinDynamicsImage(const std::vector<ImageF>& xyb1,
ImageF& result) const;
// Same as above, but the frequency decomposition was already applied.
void DiffmapPsychoImage(const PsychoImage& ps1, ImageF &result) const;
void Mask(std::vector<ImageF>* BUTTERAUGLI_RESTRICT mask,
std::vector<ImageF>* BUTTERAUGLI_RESTRICT mask_dc) const;
private:
void MaltaDiffMapLF(const ImageF& y0,
const ImageF& y1,
double w_0gt1,
double w_0lt1,
double normalization,
ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const;
void MaltaDiffMap(const ImageF& y0,
const ImageF& y1,
double w_0gt1,
double w_0lt1,
double normalization,
ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const;
ImageF CombineChannels(const std::vector<ImageF>& scale_xyb,
const std::vector<ImageF>& scale_xyb_dc,
const std::vector<ImageF>& block_diff_dc,
const std::vector<ImageF>& block_diff_ac) const;
const size_t xsize_;
const size_t ysize_;
const size_t num_pixels_;
PsychoImage pi0_;
};
void ButteraugliDiffmap(const std::vector<ImageF> &rgb0,
const std::vector<ImageF> &rgb1,
ImageF &diffmap);
double ButteraugliScoreFromDiffmap(const ImageF& distmap);
// Generate rgb-representation of the distance between two images.
void CreateHeatMapImage(const std::vector<float> &distmap,
double good_threshold, double bad_threshold,
size_t xsize, size_t ysize,
std::vector<uint8_t> *heatmap);
// Compute values of local frequency and dc masking based on the activity
// in the two images.
void Mask(const std::vector<ImageF>& xyb0,
const std::vector<ImageF>& xyb1,
std::vector<ImageF>* BUTTERAUGLI_RESTRICT mask,
std::vector<ImageF>* BUTTERAUGLI_RESTRICT mask_dc);
template <class V>
BUTTERAUGLI_INLINE void RgbToXyb(const V &r, const V &g, const V &b,
V *BUTTERAUGLI_RESTRICT valx,
V *BUTTERAUGLI_RESTRICT valy,
V *BUTTERAUGLI_RESTRICT valb) {
*valx = r - g;
*valy = r + g;
*valb = b;
}
template <class V>
BUTTERAUGLI_INLINE void OpsinAbsorbance(const V &in0, const V &in1,
const V &in2,
V *BUTTERAUGLI_RESTRICT out0,
V *BUTTERAUGLI_RESTRICT out1,
V *BUTTERAUGLI_RESTRICT out2) {
// https://en.wikipedia.org/wiki/Photopsin absorbance modeling.
static const double mixi0 = 0.254462330846;
static const double mixi1 = 0.488238255095;
static const double mixi2 = 0.0635278003854;
static const double mixi3 = 1.01681026909;
static const double mixi4 = 0.195214015766;
static const double mixi5 = 0.568019861857;
static const double mixi6 = 0.0860755536007;
static const double mixi7 = 1.1510118369;
static const double mixi8 = 0.07374607900105684;
static const double mixi9 = 0.06142425304154509;
static const double mixi10 = 0.24416850520714256;
static const double mixi11 = 1.20481945273;
const V mix0(mixi0);
const V mix1(mixi1);
const V mix2(mixi2);
const V mix3(mixi3);
const V mix4(mixi4);
const V mix5(mixi5);
const V mix6(mixi6);
const V mix7(mixi7);
const V mix8(mixi8);
const V mix9(mixi9);
const V mix10(mixi10);
const V mix11(mixi11);
*out0 = mix0 * in0 + mix1 * in1 + mix2 * in2 + mix3;
*out1 = mix4 * in0 + mix5 * in1 + mix6 * in2 + mix7;
*out2 = mix8 * in0 + mix9 * in1 + mix10 * in2 + mix11;
}
std::vector<ImageF> OpsinDynamicsImage(const std::vector<ImageF>& rgb);
ImageF Blur(const ImageF& in, float sigma, float border_ratio);
double SimpleGamma(double v);
double GammaMinArg();
double GammaMaxArg();
// Polynomial evaluation via Clenshaw's scheme (similar to Horner's).
// Template enables compile-time unrolling of the recursion, but must reside
// outside of a class due to the specialization.
template <int INDEX>
static inline void ClenshawRecursion(const double x, const double *coefficients,
double *b1, double *b2) {
const double x_b1 = x * (*b1);
const double t = (x_b1 + x_b1) - (*b2) + coefficients[INDEX];
*b2 = *b1;
*b1 = t;
ClenshawRecursion<INDEX - 1>(x, coefficients, b1, b2);
}
// Base case
template <>
inline void ClenshawRecursion<0>(const double x, const double *coefficients,
double *b1, double *b2) {
const double x_b1 = x * (*b1);
// The final iteration differs - no 2 * x_b1 here.
*b1 = x_b1 - (*b2) + coefficients[0];
}
// Rational polynomial := dividing two polynomial evaluations. These are easier
// to find than minimax polynomials.
struct RationalPolynomial {
template <int N>
static double EvaluatePolynomial(const double x,
const double (&coefficients)[N]) {
double b1 = 0.0;
double b2 = 0.0;
ClenshawRecursion<N - 1>(x, coefficients, &b1, &b2);
return b1;
}
// Evaluates the polynomial at x (in [min_value, max_value]).
inline double operator()(const double x) const {
// First normalize to [0, 1].
const double x01 = (x - min_value) / (max_value - min_value);
// And then to [-1, 1] domain of Chebyshev polynomials.
const double xc = 2.0 * x01 - 1.0;
const double yp = EvaluatePolynomial(xc, p);
const double yq = EvaluatePolynomial(xc, q);
if (yq == 0.0) return 0.0;
return static_cast<float>(yp / yq);
}
// Domain of the polynomials; they are undefined elsewhere.
double min_value;
double max_value;
// Coefficients of T_n (Chebyshev polynomials of the first kind).
// Degree 5/5 is a compromise between accuracy (0.1%) and numerical stability.
double p[5 + 1];
double q[5 + 1];
};
static inline double GammaPolynomial(double value) {
static const RationalPolynomial r = {
0.971783, 590.188894,
{
98.7821300963361, 164.273222212631, 92.948112871376,
33.8165311212688, 6.91626704983562, 0.556380877028234
},
{
1, 1.64339473427892, 0.89392405219969, 0.298947051776379,
0.0507146002577288, 0.00226495093949756
}};
return r(value);
}
} // namespace butteraugli
#endif // BUTTERAUGLI_BUTTERAUGLI_H_

View File

@ -0,0 +1,457 @@
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <vector>
#include "butteraugli/butteraugli.h"
extern "C" {
#include "png.h"
#include "jpeglib.h"
}
namespace butteraugli {
namespace {
// "rgb": cleared and filled with same-sized image planes (one per channel);
// either RGB, or RGBA if the PNG contains an alpha channel.
bool ReadPNG(FILE* f, std::vector<Image8>* rgb) {
png_structp png_ptr =
png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr) {
return false;
}
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_read_struct(&png_ptr, NULL, NULL);
return false;
}
if (setjmp(png_jmpbuf(png_ptr)) != 0) {
// Ok we are here because of the setjmp.
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return false;
}
rewind(f);
png_init_io(png_ptr, f);
// The png_transforms flags are as follows:
// packing == convert 1,2,4 bit images,
// strip == 16 -> 8 bits / channel,
// shift == use sBIT dynamics, and
// expand == palettes -> rgb, grayscale -> 8 bit images, tRNS -> alpha.
const unsigned int png_transforms =
PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16;
png_read_png(png_ptr, info_ptr, png_transforms, NULL);
png_bytep* row_pointers = png_get_rows(png_ptr, info_ptr);
const int xsize = png_get_image_width(png_ptr, info_ptr);
const int ysize = png_get_image_height(png_ptr, info_ptr);
const int components = png_get_channels(png_ptr, info_ptr);
*rgb = CreatePlanes<uint8_t>(xsize, ysize, 3);
switch (components) {
case 1: {
// GRAYSCALE
for (int y = 0; y < ysize; ++y) {
const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y];
uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y);
uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y);
uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y);
for (int x = 0; x < xsize; ++x) {
const uint8_t gray = row[x];
row0[x] = row1[x] = row2[x] = gray;
}
}
break;
}
case 2: {
// GRAYSCALE_ALPHA
rgb->push_back(Image8(xsize, ysize));
for (int y = 0; y < ysize; ++y) {
const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y];
uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y);
uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y);
uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y);
uint8_t* const BUTTERAUGLI_RESTRICT row3 = (*rgb)[3].Row(y);
for (int x = 0; x < xsize; ++x) {
const uint8_t gray = row[2 * x + 0];
const uint8_t alpha = row[2 * x + 1];
row0[x] = gray;
row1[x] = gray;
row2[x] = gray;
row3[x] = alpha;
}
}
break;
}
case 3: {
// RGB
for (int y = 0; y < ysize; ++y) {
const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y];
uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y);
uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y);
uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y);
for (int x = 0; x < xsize; ++x) {
row0[x] = row[3 * x + 0];
row1[x] = row[3 * x + 1];
row2[x] = row[3 * x + 2];
}
}
break;
}
case 4: {
// RGBA
rgb->push_back(Image8(xsize, ysize));
for (int y = 0; y < ysize; ++y) {
const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y];
uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y);
uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y);
uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y);
uint8_t* const BUTTERAUGLI_RESTRICT row3 = (*rgb)[3].Row(y);
for (int x = 0; x < xsize; ++x) {
row0[x] = row[4 * x + 0];
row1[x] = row[4 * x + 1];
row2[x] = row[4 * x + 2];
row3[x] = row[4 * x + 3];
}
}
break;
}
default:
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return false;
}
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
return true;
}
const double* NewSrgbToLinearTable() {
double* table = new double[256];
for (int i = 0; i < 256; ++i) {
const double srgb = i / 255.0;
table[i] =
255.0 * (srgb <= 0.04045 ? srgb / 12.92
: std::pow((srgb + 0.055) / 1.055, 2.4));
}
return table;
}
void jpeg_catch_error(j_common_ptr cinfo) {
(*cinfo->err->output_message) (cinfo);
jmp_buf* jpeg_jmpbuf = (jmp_buf*) cinfo->client_data;
jpeg_destroy(cinfo);
longjmp(*jpeg_jmpbuf, 1);
}
// "rgb": cleared and filled with same-sized image planes (one per channel);
// either RGB, or RGBA if the PNG contains an alpha channel.
bool ReadJPEG(FILE* f, std::vector<Image8>* rgb) {
rewind(f);
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jmp_buf jpeg_jmpbuf;
cinfo.client_data = &jpeg_jmpbuf;
jerr.error_exit = jpeg_catch_error;
if (setjmp(jpeg_jmpbuf)) {
return false;
}
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, f);
jpeg_read_header(&cinfo, TRUE);
jpeg_start_decompress(&cinfo);
int row_stride = cinfo.output_width * cinfo.output_components;
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)
((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
const size_t xsize = cinfo.output_width;
const size_t ysize = cinfo.output_height;
*rgb = CreatePlanes<uint8_t>(xsize, ysize, 3);
switch (cinfo.out_color_space) {
case JCS_GRAYSCALE:
while (cinfo.output_scanline < cinfo.output_height) {
jpeg_read_scanlines(&cinfo, buffer, 1);
const uint8_t* const BUTTERAUGLI_RESTRICT row = buffer[0];
uint8_t* const BUTTERAUGLI_RESTRICT row0 =
(*rgb)[0].Row(cinfo.output_scanline - 1);
uint8_t* const BUTTERAUGLI_RESTRICT row1 =
(*rgb)[1].Row(cinfo.output_scanline - 1);
uint8_t* const BUTTERAUGLI_RESTRICT row2 =
(*rgb)[2].Row(cinfo.output_scanline - 1);
for (int x = 0; x < xsize; x++) {
const uint8_t gray = row[x];
row0[x] = row1[x] = row2[x] = gray;
}
}
break;
case JCS_RGB:
while (cinfo.output_scanline < cinfo.output_height) {
jpeg_read_scanlines(&cinfo, buffer, 1);
const uint8_t* const BUTTERAUGLI_RESTRICT row = buffer[0];
uint8_t* const BUTTERAUGLI_RESTRICT row0 =
(*rgb)[0].Row(cinfo.output_scanline - 1);
uint8_t* const BUTTERAUGLI_RESTRICT row1 =
(*rgb)[1].Row(cinfo.output_scanline - 1);
uint8_t* const BUTTERAUGLI_RESTRICT row2 =
(*rgb)[2].Row(cinfo.output_scanline - 1);
for (int x = 0; x < xsize; x++) {
row0[x] = row[3 * x + 0];
row1[x] = row[3 * x + 1];
row2[x] = row[3 * x + 2];
}
}
break;
default:
jpeg_destroy_decompress(&cinfo);
return false;
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
return true;
}
// Translate R, G, B channels from sRGB to linear space. If an alpha channel
// is present, overlay the image over a black or white background. Overlaying
// is done in the sRGB space; while technically incorrect, this is aligned with
// many other software (web browsers, WebP near lossless).
void FromSrgbToLinear(const std::vector<Image8>& rgb,
std::vector<ImageF>& linear, int background) {
const size_t xsize = rgb[0].xsize();
const size_t ysize = rgb[0].ysize();
static const double* const kSrgbToLinearTable = NewSrgbToLinearTable();
if (rgb.size() == 3) { // RGB
for (int c = 0; c < 3; c++) {
linear.push_back(ImageF(xsize, ysize));
for (int y = 0; y < ysize; ++y) {
const uint8_t* const BUTTERAUGLI_RESTRICT row_rgb = rgb[c].Row(y);
float* const BUTTERAUGLI_RESTRICT row_linear = linear[c].Row(y);
for (size_t x = 0; x < xsize; x++) {
const int value = row_rgb[x];
row_linear[x] = kSrgbToLinearTable[value];
}
}
}
} else { // RGBA
for (int c = 0; c < 3; c++) {
linear.push_back(ImageF(xsize, ysize));
for (int y = 0; y < ysize; ++y) {
const uint8_t* const BUTTERAUGLI_RESTRICT row_rgb = rgb[c].Row(y);
float* const BUTTERAUGLI_RESTRICT row_linear = linear[c].Row(y);
const uint8_t* const BUTTERAUGLI_RESTRICT row_alpha = rgb[3].Row(y);
for (size_t x = 0; x < xsize; x++) {
int value;
if (row_alpha[x] == 255) {
value = row_rgb[x];
} else if (row_alpha[x] == 0) {
value = background;
} else {
const int fg_weight = row_alpha[x];
const int bg_weight = 255 - fg_weight;
value =
(row_rgb[x] * fg_weight + background * bg_weight + 127) / 255;
}
row_linear[x] = kSrgbToLinearTable[value];
}
}
}
}
}
std::vector<Image8> ReadImageOrDie(const char* filename) {
std::vector<Image8> rgb;
FILE* f = fopen(filename, "rb");
if (!f) {
fprintf(stderr, "Cannot open %s\n", filename);
exit(1);
}
unsigned char magic[2];
if (fread(magic, 1, 2, f) != 2) {
fprintf(stderr, "Cannot read from %s\n", filename);
exit(1);
}
if (magic[0] == 0xFF && magic[1] == 0xD8) {
if (!ReadJPEG(f, &rgb)) {
fprintf(stderr, "File %s is a malformed JPEG.\n", filename);
exit(1);
}
} else {
if (!ReadPNG(f, &rgb)) {
fprintf(stderr, "File %s is neither a valid JPEG nor a valid PNG.\n",
filename);
exit(1);
}
}
fclose(f);
return rgb;
}
static void ScoreToRgb(double score, double good_threshold,
double bad_threshold, uint8_t rgb[3]) {
double heatmap[12][3] = {
{ 0, 0, 0 },
{ 0, 0, 1 },
{ 0, 1, 1 },
{ 0, 1, 0 }, // Good level
{ 1, 1, 0 },
{ 1, 0, 0 }, // Bad level
{ 1, 0, 1 },
{ 0.5, 0.5, 1.0 },
{ 1.0, 0.5, 0.5 }, // Pastel colors for the very bad quality range.
{ 1.0, 1.0, 0.5 },
{ 1, 1, 1, },
{ 1, 1, 1, },
};
if (score < good_threshold) {
score = (score / good_threshold) * 0.3;
} else if (score < bad_threshold) {
score = 0.3 + (score - good_threshold) /
(bad_threshold - good_threshold) * 0.15;
} else {
score = 0.45 + (score - bad_threshold) /
(bad_threshold * 12) * 0.5;
}
static const int kTableSize = sizeof(heatmap) / sizeof(heatmap[0]);
score = std::min<double>(std::max<double>(
score * (kTableSize - 1), 0.0), kTableSize - 2);
int ix = static_cast<int>(score);
double mix = score - ix;
for (int i = 0; i < 3; ++i) {
double v = mix * heatmap[ix + 1][i] + (1 - mix) * heatmap[ix][i];
rgb[i] = static_cast<uint8_t>(255 * pow(v, 0.5) + 0.5);
}
}
void CreateHeatMapImage(const ImageF& distmap, double good_threshold,
double bad_threshold, size_t xsize, size_t ysize,
std::vector<uint8_t>* heatmap) {
heatmap->resize(3 * xsize * ysize);
for (size_t y = 0; y < ysize; ++y) {
for (size_t x = 0; x < xsize; ++x) {
int px = xsize * y + x;
double d = distmap.Row(y)[x];
uint8_t* rgb = &(*heatmap)[3 * px];
ScoreToRgb(d, good_threshold, bad_threshold, rgb);
}
}
}
// main() function, within butteraugli namespace for convenience.
int Run(int argc, char* argv[]) {
if (argc != 3 && argc != 4) {
fprintf(stderr,
"Usage: %s {image1.(png|jpg|jpeg)} {image2.(png|jpg|jpeg)} "
"[heatmap.ppm]\n",
argv[0]);
return 1;
}
std::vector<Image8> rgb1 = ReadImageOrDie(argv[1]);
std::vector<Image8> rgb2 = ReadImageOrDie(argv[2]);
if (rgb1.size() != rgb2.size()) {
fprintf(stderr, "Different number of channels: %lu vs %lu\n", rgb1.size(),
rgb2.size());
exit(1);
}
for (size_t c = 0; c < rgb1.size(); ++c) {
if (rgb1[c].xsize() != rgb2[c].xsize() ||
rgb1[c].ysize() != rgb2[c].ysize()) {
fprintf(
stderr, "The images are not equal in size: (%lu,%lu) vs (%lu,%lu)\n",
rgb1[c].xsize(), rgb2[c].xsize(), rgb1[c].ysize(), rgb2[c].ysize());
return 1;
}
}
// TODO: Figure out if it is a good idea to fetch the gamma from the image
// instead of applying sRGB conversion.
std::vector<ImageF> linear1, linear2;
// Overlay the image over a black background.
FromSrgbToLinear(rgb1, linear1, 0);
FromSrgbToLinear(rgb2, linear2, 0);
ImageF diff_map, diff_map_on_white;
double diff_value;
if (!butteraugli::ButteraugliInterface(linear1, linear2, diff_map,
diff_value)) {
fprintf(stderr, "Butteraugli comparison failed\n");
return 1;
}
ImageF* diff_map_ptr = &diff_map;
if (rgb1.size() == 4 || rgb2.size() == 4) {
// If the alpha channel is present, overlay the image over a white
// background as well.
FromSrgbToLinear(rgb1, linear1, 255);
FromSrgbToLinear(rgb2, linear2, 255);
double diff_value_on_white;
if (!butteraugli::ButteraugliInterface(linear1, linear2, diff_map_on_white,
diff_value_on_white)) {
fprintf(stderr, "Butteraugli comparison failed\n");
return 1;
}
if (diff_value_on_white > diff_value) {
diff_value = diff_value_on_white;
diff_map_ptr = &diff_map_on_white;
}
}
printf("%lf\n", diff_value);
if (argc == 4) {
const double good_quality = ::butteraugli::ButteraugliFuzzyInverse(1.5);
const double bad_quality = ::butteraugli::ButteraugliFuzzyInverse(0.5);
std::vector<uint8_t> rgb;
CreateHeatMapImage(*diff_map_ptr, good_quality, bad_quality,
rgb1[0].xsize(), rgb2[0].ysize(), &rgb);
FILE* const fmap = fopen(argv[3], "wb");
if (fmap == NULL) {
fprintf(stderr, "Cannot open %s\n", argv[3]);
perror("fopen");
return 1;
}
bool ok = true;
if (fprintf(fmap, "P6\n%lu %lu\n255\n",
rgb1[0].xsize(), rgb1[0].ysize()) < 0){
perror("fprintf");
ok = false;
}
if (ok && fwrite(rgb.data(), 1, rgb.size(), fmap) != rgb.size()) {
perror("fwrite");
ok = false;
}
if (fclose(fmap) != 0) {
perror("fclose");
ok = false;
}
if (!ok) return 1;
}
return 0;
}
} // namespace
} // namespace butteraugli
int main(int argc, char** argv) { return butteraugli::Run(argc, argv); }

View File

@ -0,0 +1,89 @@
# Description:
# The Independent JPEG Group's JPEG runtime library.
licenses(["notice"]) # custom notice-style license, see LICENSE
cc_library(
name = "jpeg",
srcs = [
"cderror.h",
"cdjpeg.h",
"jaricom.c",
"jcapimin.c",
"jcapistd.c",
"jcarith.c",
"jccoefct.c",
"jccolor.c",
"jcdctmgr.c",
"jchuff.c",
"jcinit.c",
"jcmainct.c",
"jcmarker.c",
"jcmaster.c",
"jcomapi.c",
"jconfig.h",
"jcparam.c",
"jcprepct.c",
"jcsample.c",
"jctrans.c",
"jdapimin.c",
"jdapistd.c",
"jdarith.c",
"jdatadst.c",
"jdatasrc.c",
"jdcoefct.c",
"jdcolor.c",
"jdct.h",
"jddctmgr.c",
"jdhuff.c",
"jdinput.c",
"jdmainct.c",
"jdmarker.c",
"jdmaster.c",
"jdmerge.c",
"jdpostct.c",
"jdsample.c",
"jdtrans.c",
"jerror.c",
"jfdctflt.c",
"jfdctfst.c",
"jfdctint.c",
"jidctflt.c",
"jidctfst.c",
"jidctint.c",
"jinclude.h",
"jmemmgr.c",
"jmemnobs.c",
"jmemsys.h",
"jmorecfg.h",
"jquant1.c",
"jquant2.c",
"jutils.c",
"jversion.h",
"transupp.h",
],
hdrs = [
"jerror.h",
"jpegint.h",
"jpeglib.h",
],
includes = ["."],
visibility = ["//visibility:public"],
)
genrule(
name = "configure",
outs = ["jconfig.h"],
cmd = "cat <<EOF >$@\n" +
"#define HAVE_PROTOTYPES 1\n" +
"#define HAVE_UNSIGNED_CHAR 1\n" +
"#define HAVE_UNSIGNED_SHORT 1\n" +
"#define HAVE_STDDEF_H 1\n" +
"#define HAVE_STDLIB_H 1\n" +
"#ifdef WIN32\n" +
"#define INLINE __inline\n" +
"#else\n" +
"#define INLINE __inline__\n" +
"#endif\n" +
"EOF\n",
)

View File

@ -0,0 +1,33 @@
# Description:
# libpng is the official PNG reference library.
licenses(["notice"]) # BSD/MIT-like license
cc_library(
name = "png",
srcs = [
"png.c",
"pngerror.c",
"pngget.c",
"pngmem.c",
"pngpread.c",
"pngread.c",
"pngrio.c",
"pngrtran.c",
"pngrutil.c",
"pngset.c",
"pngtrans.c",
"pngwio.c",
"pngwrite.c",
"pngwtran.c",
"pngwutil.c",
],
hdrs = [
"png.h",
"pngconf.h",
],
includes = ["."],
linkopts = ["-lm"],
visibility = ["//visibility:public"],
deps = ["@zlib_archive//:zlib"],
)

View File

@ -0,0 +1,36 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"]) # BSD/MIT-like license (for zlib)
cc_library(
name = "zlib",
srcs = [
"adler32.c",
"compress.c",
"crc32.c",
"crc32.h",
"deflate.c",
"deflate.h",
"gzclose.c",
"gzguts.h",
"gzlib.c",
"gzread.c",
"gzwrite.c",
"infback.c",
"inffast.c",
"inffast.h",
"inffixed.h",
"inflate.c",
"inflate.h",
"inftrees.c",
"inftrees.h",
"trees.c",
"trees.h",
"uncompr.c",
"zconf.h",
"zutil.c",
"zutil.h",
],
hdrs = ["zlib.h"],
includes = ["."],
)