blob: c1cf789f766ebf107851ff8cb3d4b1882b4b254a [file] [log] [blame]
// Copyright 2019 Google LLC
//
// 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
//
// https://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.
// -----------------------------------------------------------------------------
//
// main entry for the lossless encoder.
//
// Author: Vincent Rabaud (vrabaud@google.com)
#include <algorithm>
#include <array>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <limits>
#include "src/common/color_precision.h"
#include "src/common/lossless/transforms.h"
#include "src/common/progress_watcher.h"
#include "src/dsp/lossless/dspl.h"
#include "src/dsp/math.h"
#include "src/enc/lossless/backward_references_enc.h"
#include "src/enc/lossless/losslessi_enc.h"
#include "src/enc/lossless/palette.h"
#include "src/utils/ans_utils.h"
#include "src/utils/hash.h"
#include "src/utils/utils.h"
#include "src/utils/vector.h"
#include "src/wp2/base.h"
#include "src/wp2/encode.h"
#include "src/wp2/format_constants.h"
namespace WP2L {
// -----------------------------------------------------------------------------
// Entropy analysis
namespace {
// These five modes are evaluated and their respective entropy is computed.
enum class EntropyMode {
// No transform, colors are stored as is.
kDirect,
// Spatial prediction. We predict the next pixel (using one of many prediction
// modes) and store the difference between the prediction and the actual value
// (= "the residual").
kSpatial,
// Instead of storing red, green, and blue, we store red minus green, green,
// and blue minus green (takes advantage of correlation between the three
// channels).
kSubGreen,
// We use both kSpatial and kSubGreen a the same time.
kSpatialSubGreen,
// We create a palette of colors and store one palette index per pixel.
kPalette,
// Same as kPalette except that we then apply predictors on the indices
// themselves.
kPaletteSpatial,
kNum
};
// Histogram types.
enum HistoIdx {
kHistoAlpha = 0,
kHistoAlphaPred, // Alpha difference with previous pixel.
kHistoGreen,
kHistoGreenPred, // Green difference with previous pixel.
kHistoRed,
kHistoRedPred, // Red difference with previous pixel.
kHistoBlue,
kHistoBluePred, // Blue difference with previous pixel.
kHistoRedSubGreen, // Red minus green.
kHistoRedPredSubGreen, // Red minus green difference with previous pixel.
kHistoBlueSubGreen, // Blue minus green.
kHistoBluePredSubGreen, // Blue minus green difference with previous pixel.
kHistoPalette,
kHistoNum
};
// Lightweight histogram, not owning data.
struct Histogram {
// Adds v to 'data' by calling MakeIndex and clamping it to fit in 'len'.
void AddSigned(int16_t v) {
++data[WP2::Clamp((int)MakeIndex(v), 0, len - 1)];
}
uint32_t* data;
int len;
};
// Adds the red minus green, and blue minus green values from the given argb
// pixel 'p' to the given histograms 'r' and 'b'.
template <typename T>
void AddSingleSubGreen(const T* const p, Histogram* const r,
Histogram* const b) {
const int green = p[2]; // The upper bits are masked away later.
r->AddSigned(p[1] - green);
b->AddSigned(p[3] - green);
}
// Adds the values from the given argb pixel 'p' to the given histograms 'a',
// 'r', 'g', and 'b'.
template <typename T>
void AddSingleSigned(const T* const p, Histogram* const a, Histogram* const r,
Histogram* const g, Histogram* const b) {
a->AddSigned(p[0]);
r->AddSigned(p[1]);
g->AddSigned(p[2]);
b->AddSigned(p[3]);
}
template <typename T>
void AddSingleUnsigned(const T* const p, Histogram* const a, Histogram* const r,
Histogram* const g, Histogram* const b) {
assert(p[0] >= 0 && p[0] < a->len && p[1] >= 0 && p[1] < r->len);
assert(p[2] >= 0 && p[2] < g->len && p[3] >= 0 && p[3] < b->len);
++a->data[p[0]];
++r->data[p[1]];
++g->data[p[2]];
++b->data[p[3]];
}
template <typename T>
void Premultiply(const T* const src, T* const dst) {
const uint32_t a = src[0];
if (a < 255) {
dst[0] = src[0];
dst[1] = WP2::DivBy255(src[1] * a);
dst[2] = WP2::DivBy255(src[2] * a);
dst[3] = WP2::DivBy255(src[3] * a);
} else {
std::copy(src, src + 4, dst);
}
}
// Local struct containing the final config and local info for sorting.
struct EntropyConfig {
CrunchConfig config;
EntropyMode mode;
float entropy;
};
// Find the corresponding combination of transforms in the reference table.
WP2Status FindEncodingRecipeImpl(EncodingAlgorithm algorithm,
CrunchConfig* config,
const TransformType* const transforms,
uint32_t size) {
assert(size <= kPossibleTransformCombinationSize);
for (const EncodingRecipe& recipe : kPossibleEncodingRecipes) {
if (recipe.algorithm != algorithm) continue;
if (std::equal(transforms, transforms + size, recipe.transforms) &&
(size == kPossibleTransformCombinationSize ||
recipe.transforms[size] == TransformType::kNum)) {
config->algorithm = algorithm;
for (uint32_t i = 0; i < kPossibleTransformCombinationSize; ++i) {
config->transforms[i].type = recipe.transforms[i];
}
return WP2_STATUS_OK;
}
}
return WP2_STATUS_INVALID_PARAMETER;
}
// Find a set of TransformType's into the official list and returns a pointer to
// it.
template <typename... Ts>
WP2Status FindEncodingRecipe(EncodingAlgorithm algorithm, CrunchConfig* config,
Ts... args) {
const TransformType transforms[] = {args..., TransformType::kNum};
WP2_CHECK_STATUS(
FindEncodingRecipeImpl(algorithm, config, transforms, sizeof...(args)));
return WP2_STATUS_OK;
}
WP2Status AddWebPConfig(EntropyMode mode, bool use_premultiplied,
bool red_and_blue_always_zero, float entropy,
int effort, WP2::Vector<EntropyConfig>& configs) {
EntropyConfig c;
c.mode = mode;
c.entropy = entropy;
c.config.use_premultiplied = use_premultiplied;
TransformType transforms[kPossibleTransformCombinationSize];
uint32_t size = 0;
if (mode == EntropyMode::kPalette || mode == EntropyMode::kPaletteSpatial) {
transforms[size++] = TransformType::kColorIndexing;
red_and_blue_always_zero = true;
}
if (mode == EntropyMode::kSubGreen || mode == EntropyMode::kSpatialSubGreen) {
transforms[size++] = TransformType::kSubtractGreen;
}
if (mode == EntropyMode::kSpatial || mode == EntropyMode::kSpatialSubGreen ||
mode == EntropyMode::kPaletteSpatial) {
transforms[size++] = TransformType::kPredictor;
if (effort != 0 && !red_and_blue_always_zero) {
transforms[size++] = TransformType::kCrossColor;
}
}
WP2_CHECK_STATUS(FindEncodingRecipeImpl(EncodingAlgorithm::kWebP, &c.config,
transforms, size));
WP2_CHECK_ALLOC_OK(configs.push_back(c));
return WP2_STATUS_OK;
}
// Analyzes the distribution of colors on the image to estimate the best
// transform configurations.
// Returns at most 'num_configs_max' sorted from best to worst in 'configs'.
template <typename T>
WP2Status AnalyzeEntropyImpl(const T* argb, bool can_use_palette, int effort,
uint32_t transform_bits, const Encoder& encoder,
uint32_t num_configs_max,
WP2::Vector<EntropyConfig>& configs) {
assert(num_configs_max >= 1);
const WP2::ArgbBuffer& pic = encoder.pic_;
const uint32_t stride = pic.stride(), width = pic.width(),
height = pic.height();
const WP2SampleFormat format = pic.format();
const bool can_premultiply =
(encoder.has_alpha_ && !WP2IsPremultiplied(format) &&
!encoder.config_.keep_unmultiplied);
if (can_use_palette && encoder.palette_.Size() <= 16) {
// In practice, small palettes are better than any other transform.
WP2_CHECK_ALLOC_OK(configs.reserve(1));
for (EntropyMode mode :
{EntropyMode::kPalette, EntropyMode::kPaletteSpatial}) {
WP2_CHECK_STATUS(AddWebPConfig(mode, can_premultiply,
/*red_and_blue_always_zero=*/true,
/*entropy=*/0.f, effort, configs));
}
return WP2_STATUS_OK;
}
// Allocate histogram set with cache_bits = 0.
const uint32_t hash_bits =
can_use_palette ? WP2Log2Ceil_k(encoder.palette_.Size()) : 0;
Histogram histo[kHistoNum];
// Default histogram length for signed transforms.
const uint32_t kDefaultLen =
1 + std::max(MakeIndex(WP2::FormatMax(format, /*channel=*/1)),
MakeIndex(-(int16_t)WP2::FormatMax(format, /*channel=*/1)));
// Define the lengths of the histograms.
histo[kHistoAlpha].len = WP2::kAlphaMax + 1;
histo[kHistoAlphaPred].len = kDefaultLen;
histo[kHistoGreen].len = WP2::FormatMax(format, /*channel=*/2) + 1;
histo[kHistoGreenPred].len = kDefaultLen;
histo[kHistoRed].len = WP2::FormatMax(format, /*channel=*/1) + 1;
histo[kHistoRedPred].len = kDefaultLen;
histo[kHistoBlue].len = WP2::FormatMax(format, /*channel=*/3) + 1;
histo[kHistoBluePred].len = kDefaultLen;
histo[kHistoRedSubGreen].len = kDefaultLen;
histo[kHistoRedPredSubGreen].len = kDefaultLen;
histo[kHistoBlueSubGreen].len = kDefaultLen;
histo[kHistoBluePredSubGreen].len = kDefaultLen;
histo[kHistoPalette].len = (1 << hash_bits);
// Define the vector that will contain all the histograms.
uint32_t histo_len = 0;
for (const Histogram& h : histo) histo_len += h.len;
WP2::Vector_u32 histo_raw;
WP2_CHECK_ALLOC_OK(histo_raw.resize(histo_len));
// Define the pointers of the histograms.
histo_len = 0;
for (Histogram& h : histo) {
h.data = &histo_raw[histo_len];
histo_len += h.len;
}
WP2_CHECK_ALLOC_OK(configs.reserve(2 * (uint32_t)EntropyMode::kNum));
for (bool premult : {false, true}) {
if (!can_premultiply && premult) {
// No need to test pre-multiply if the format is already pre-multiplied
// or has no alpha.
continue;
}
// Palette is always pre-multiplied.
const bool analyze_palette =
(can_use_palette && (premult || !can_premultiply));
// Reset histograms.
std::fill(histo_raw.begin(), histo_raw.end(), 0);
// Update histograms.
constexpr T kTransparent[4] = {0, 0, 0, 0};
const T* prev_row = nullptr;
const T* curr_row = argb;
// Skip the first pixel.
T pix_prev[4] = {curr_row[0], curr_row[1], curr_row[2], curr_row[3]};
if (premult) {
Premultiply(pix_prev, pix_prev);
}
T pix_premult_prev_row[4];
T pix_premult[4];
for (uint32_t y = 0; y < height; ++y) {
for (uint32_t x = 0; x < width; ++x) {
const T* pix = &curr_row[4 * x];
const T* p;
const T* p_prev = pix_prev;
const T* p_prev_row;
if (premult) { // pixels premultiplied by us
Premultiply(pix, pix_premult);
if (y > 0) Premultiply(&prev_row[4 * x], pix_premult_prev_row);
p = pix_premult;
p_prev_row = (y > 0) ? pix_premult_prev_row : nullptr;
} else { // pixels as is
p = pix;
p_prev_row = (y > 0) ? &prev_row[4 * x] : nullptr;
// Zero-out fully transparent pixels.
if (p[0] == 0) p = kTransparent;
if (p_prev[0] == 0) p_prev = kTransparent;
if (p_prev_row != nullptr && p_prev_row[0] == 0) {
p_prev_row = kTransparent;
}
}
// Compute the difference between the two pixels.
int16_t p_diff[4];
for (uint32_t c = 0; c < 4; ++c) {
p_diff[c] = (int16_t)p[c] - (int16_t)p_prev[c];
}
std::copy(p, p + 4, pix_prev);
if ((p_diff[0] == 0 && p_diff[1] == 0 && p_diff[2] == 0 &&
p_diff[3] == 0) ||
(p_prev_row != nullptr &&
memcmp(p, p_prev_row, 4 * sizeof(T)) == 0)) {
continue;
}
AddSingleUnsigned(p, &histo[kHistoAlpha], &histo[kHistoRed],
&histo[kHistoGreen], &histo[kHistoBlue]);
AddSingleSigned(p_diff, &histo[kHistoAlphaPred], &histo[kHistoRedPred],
&histo[kHistoGreenPred], &histo[kHistoBluePred]);
AddSingleSubGreen(p, &histo[kHistoRedSubGreen],
&histo[kHistoBlueSubGreen]);
AddSingleSubGreen(p_diff, &histo[kHistoRedPredSubGreen],
&histo[kHistoBluePredSubGreen]);
// Palettes are always premultiplied. It doesn't make sense to look at
// entropy for unmultiplied palettes.
if (analyze_palette) {
// Approximate the palette by the entropy of the multiplicative hash.
const uint32_t hash = WP2::encoding::HashPix(p, hash_bits);
assert((int)hash < histo[kHistoPalette].len);
++histo[kHistoPalette].data[hash];
}
}
prev_row = curr_row;
curr_row = (const T*)((const uint8_t*)curr_row + stride);
}
// Compute the entropy of the histograms.
float entropy[kHistoNum];
// Let's add one zero to the predicted histograms. The zeros are removed
// too efficiently by the pix_diff == 0 comparison, at least one of the
// zeros is likely to exist.
++histo[kHistoRedPredSubGreen].data[0];
++histo[kHistoBluePredSubGreen].data[0];
++histo[kHistoRedPred].data[0];
++histo[kHistoGreenPred].data[0];
++histo[kHistoBluePred].data[0];
++histo[kHistoAlphaPred].data[0];
for (uint32_t j = 0; j < kHistoNum; ++j) {
entropy[j] = WP2::ANSCountsCost(&histo[j].data[0], histo[j].len);
}
// Compute the entropy of the different entropy modes.
WP2_CHECK_STATUS(
AddWebPConfig(EntropyMode::kDirect, premult,
entropy[kHistoRed] + entropy[kHistoBlue] == 0.f,
entropy[kHistoAlpha] + entropy[kHistoRed] +
entropy[kHistoGreen] + entropy[kHistoBlue],
effort, configs));
WP2_CHECK_STATUS(
AddWebPConfig(EntropyMode::kSpatial, premult,
entropy[kHistoRedPred] + entropy[kHistoBluePred] == 0.f,
entropy[kHistoAlphaPred] + entropy[kHistoRedPred] +
entropy[kHistoGreenPred] + entropy[kHistoBluePred],
effort, configs));
WP2_CHECK_STATUS(AddWebPConfig(
EntropyMode::kSubGreen, premult,
entropy[kHistoRedSubGreen] + entropy[kHistoBlueSubGreen] == 0.f,
entropy[kHistoAlpha] + entropy[kHistoRedSubGreen] +
entropy[kHistoGreen] + entropy[kHistoBlueSubGreen],
effort, configs));
WP2_CHECK_STATUS(AddWebPConfig(
EntropyMode::kSpatialSubGreen, premult,
entropy[kHistoRedPredSubGreen] + entropy[kHistoBluePredSubGreen] == 0.f,
entropy[kHistoAlphaPred] + entropy[kHistoRedPredSubGreen] +
entropy[kHistoGreenPred] + entropy[kHistoBluePredSubGreen],
effort, configs));
// Palettes are always premultiplied. It doesn't make sense to look at
// entropy for unmultiplied palettes, except if the format does not allow
// it.
if (analyze_palette) {
WP2_CHECK_STATUS(AddWebPConfig(EntropyMode::kPalette, premult,
/*red_and_blue_always_zero=*/true,
entropy[kHistoPalette], effort, configs));
}
}
for (const EntropyConfig& c : configs) {
(void)c;
if (!can_use_palette) assert(c.mode != EntropyMode::kPalette);
if (!can_premultiply) assert(!c.config.use_premultiplied);
}
// Add extra bit costs.
const uint32_t transform_image_size = SubSampleSize(width, transform_bits) *
SubSampleSize(height, transform_bits);
for (EntropyConfig& c : configs) {
for (uint32_t i = 0; i < c.config.GetNumTransforms(); ++i) {
if (c.config.transforms[i].type == TransformType::kPredictor) {
// When including transforms, there is an overhead in bits from
// storing them. This overhead is small but matters for small images.
// For spatial, there are 14 transformations.
c.entropy += transform_image_size * WP2Log2(14);
} else if (c.config.transforms[i].type == TransformType::kCrossColor) {
// For color transforms, estimating the cross color transform entropy is
// hard and easy to overshoot. Adding the cost of the transform image is
// actually worsening results so we are not artificially adding it like
// we do for kPredictor.
} else if (c.config.transforms[i].type == TransformType::kColorIndexing) {
// For palettes, add the cost of storing the palette.
c.entropy += encoder.palette_.GetCost();
}
}
}
// Sort the configs.
std::stable_sort(configs.begin(), configs.end(),
[](const EntropyConfig& c1, const EntropyConfig& c2) {
return (c1.entropy < c2.entropy);
});
// Only keep the top num_configs_max ones.
configs.resize_no_check(std::min((uint32_t)configs.size(), num_configs_max));
// In case we have a kColorIndexing transform, also add kColorIndexing +
// kPredictor.
for (const EntropyConfig& c : configs) {
if (c.config.transforms[0].type == TransformType::kColorIndexing &&
c.config.GetNumTransforms() == 1) {
WP2_CHECK_STATUS(AddWebPConfig(
EntropyMode::kPaletteSpatial, c.config.use_premultiplied,
/*red_and_blue_always_zero=*/true, c.entropy, effort, configs));
break;
}
}
return WP2_STATUS_OK;
}
WP2Status AnalyzeEntropy(bool can_use_palette, int effort,
uint32_t transform_bits, const Encoder& encoder,
uint32_t num_configs_max,
WP2::Vector<EntropyConfig>& configs) {
if (WP2Formatbpc(encoder.pic_.format()) <= 8) {
WP2_CHECK_STATUS(AnalyzeEntropyImpl(encoder.pic_.GetRow8(0),
can_use_palette, effort, transform_bits,
encoder, num_configs_max, configs));
} else {
WP2_CHECK_STATUS(AnalyzeEntropyImpl(encoder.pic_.GetRow16(0),
can_use_palette, effort, transform_bits,
encoder, num_configs_max, configs));
}
return WP2_STATUS_OK;
}
static int GetHistoBits(int effort, bool use_palette, int width, int height) {
// Make tile size a function of encoding method (Range: 0 to 6).
// TODO(vrabaud) use rounding. We leave it as is so that for the default
// value of 5, it matches the old default value of 3.
uint32_t histo_bits = (use_palette ? 9. : 7.) - effort * 6. / 9.;
while (true) {
const uint32_t huff_image_size =
SubSampleSize(width, histo_bits) * SubSampleSize(height, histo_bits);
if (huff_image_size <= kMaxHistogramImageSize) break;
++histo_bits;
}
return WP2::Clamp(histo_bits, kHistogramBitsMin, kHistogramBitsMax);
}
uint32_t GetTransformBits(uint32_t effort, uint32_t histo_bits) {
const uint32_t max_transform_bits = (effort < 5) ? 6 : (effort > 5) ? 4 : 5;
const uint32_t res =
(histo_bits > max_transform_bits) ? max_transform_bits : histo_bits;
assert(res <= kTransformBitsMax);
return res;
}
// Fills in 'crunch_configs' with a set of transforms and LZ77 variants to try
// based on heuristics. Also initializes the palette in enc->palette_ and
// sets red_and_blue_always_zero to true if the red and blue components are
// always zero after applying the best transform.
// TODO(maryla): the logic for red_and_blue_always_zero seems broken: it's only
// valid if there's only one transform (effort != 9)
WP2Status EncoderAnalyzeDefault(const WP2::ProgressRange& progress,
const Encoder& encoder,
WP2::Vector<CrunchConfig>* const configs) {
const WP2::ArgbBuffer& pic = encoder.pic_;
const int effort = encoder.config_.effort;
// Deal with forced lossless methods.
uint32_t n_lz77s;
assert(!pic.IsEmpty());
// Do not use a palette when there is only one color as ANS will optimize it
// anyway.
const bool can_use_palette = (encoder.palette_.Size() > 1);
const bool can_premultiply =
(encoder.has_alpha_ && !WP2IsPremultiplied(encoder.pic_.format()) &&
!encoder.config_.keep_unmultiplied);
// Though inefficient, we force the use of LZW if it is requested.
if (encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kLZW) {
WP2_CHECK_ALLOC_OK(configs->resize(1));
WP2_CHECK_STATUS(
FindEncodingRecipe(EncodingAlgorithm::kLZW, &configs->back()));
configs->back().use_premultiplied = can_premultiply;
return WP2_STATUS_OK;
}
// TODO(vrabaud): Enable for all efforts once it gets better.
if (WP2Formatbpc(pic.format()) == 8 &&
!WP2IsPremultiplied(encoder.pic_.format()) &&
encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kCALIC) {
// Add a few more than the predefined ones.
std::array<std::array<uint32_t, 2>, kCommonCrossColorIndices.size() + 4>
y_uv_indices;
for (uint32_t i = 0; i < kCommonCrossColorIndices.size(); ++i) {
y_uv_indices[i] = {kCommonCrossColorIndices[i].first,
kCommonCrossColorIndices[i].second};
}
// Taken from Table VI in "Multiplierless reversible colour transforms
// and their automatic selection for image data compression" by Tilo Strutz.
y_uv_indices[kCommonCrossColorIndices.size() + 0] = {4, 4};
y_uv_indices[kCommonCrossColorIndices.size() + 1] = {7, 7};
y_uv_indices[kCommonCrossColorIndices.size() + 2] = {9, 10};
y_uv_indices[kCommonCrossColorIndices.size() + 3] = {1, 10};
for (const auto& indices : y_uv_indices) {
WP2_CHECK_ALLOC_OK(configs->push_back(CrunchConfig()));
auto& config = configs->back();
WP2_CHECK_STATUS(FindEncodingRecipe(
WP2L::EncodingAlgorithm::kCALIC, &config,
TransformType::kCrossColorGlobal, TransformType::kNormalizeChannels));
config.transforms[0].cc_global_y_index = indices[0];
config.transforms[0].cc_global_uv_index = indices[1];
config.use_premultiplied = can_premultiply;
}
// Add with no channel decorrelation.
WP2_CHECK_ALLOC_OK(configs->push_back(CrunchConfig()));
auto& config = configs->back();
WP2_CHECK_STATUS(FindEncodingRecipe(WP2L::EncodingAlgorithm::kCALIC,
&config,
TransformType::kNormalizeChannels));
config.use_premultiplied = can_premultiply;
if (encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kCALIC) {
return WP2_STATUS_OK;
}
}
// TODO(vrabaud): Disable this once CALIC gets better.
if (WP2Formatbpc(pic.format()) == 8 &&
!WP2IsPremultiplied(encoder.pic_.format()) &&
(encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kSCP ||
effort == WP2::kMaxEffort)) {
// Add a few more than the predefined ones.
std::array<std::array<uint32_t, 2>, kCommonCrossColorIndices.size() + 4>
y_uv_indices;
for (uint32_t i = 0; i < kCommonCrossColorIndices.size(); ++i) {
y_uv_indices[i] = {kCommonCrossColorIndices[i].first,
kCommonCrossColorIndices[i].second};
}
// Taken from Table VI in "Multiplierless reversible colour transforms
// and their automatic selection for image data compression" by Tilo Strutz.
y_uv_indices[kCommonCrossColorIndices.size() + 0] = {4, 4};
y_uv_indices[kCommonCrossColorIndices.size() + 1] = {7, 7};
y_uv_indices[kCommonCrossColorIndices.size() + 2] = {9, 10};
y_uv_indices[kCommonCrossColorIndices.size() + 3] = {1, 10};
for (const auto& indices : y_uv_indices) {
WP2_CHECK_ALLOC_OK(configs->push_back(CrunchConfig()));
auto& config = configs->back();
WP2_CHECK_STATUS(FindEncodingRecipe(
WP2L::EncodingAlgorithm::kSCP, &config,
TransformType::kCrossColorGlobal, TransformType::kNormalizeChannels));
config.transforms[0].cc_global_y_index = indices[0];
config.transforms[0].cc_global_uv_index = indices[1];
config.use_premultiplied = can_premultiply;
}
// Add with no channel decorrelation.
WP2_CHECK_ALLOC_OK(configs->push_back(CrunchConfig()));
auto& config = configs->back();
WP2_CHECK_STATUS(FindEncodingRecipe(WP2L::EncodingAlgorithm::kSCP, &config,
TransformType::kNormalizeChannels));
config.use_premultiplied = can_premultiply;
if (encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kSCP) {
return WP2_STATUS_OK;
}
}
// Try LZW within the same palette size range condition as Group4.
if (encoder.palette_.Size() >= 2 && encoder.palette_.Size() < 256) {
WP2_CHECK_ALLOC_OK(configs->resize(configs->size() + 1));
WP2_CHECK_STATUS(
FindEncodingRecipe(EncodingAlgorithm::kLZW, &configs->back()));
configs->back().use_premultiplied = can_premultiply;
}
// Do not use Group4 for big palettes for CPU reasons and also because it is
// less likely that Group4 will work.
if (encoder.palette_.Size() >= 2 && encoder.palette_.Size() < 256) {
// Try group4 for images with a palette.
WP2_CHECK_ALLOC_OK(configs->resize(configs->size() + 1));
WP2_CHECK_STATUS(FindEncodingRecipe(EncodingAlgorithm::kGroup4,
&configs->back(),
TransformType::kColorIndexing));
configs->back().group4_use_move_to_front = false;
configs->back().use_premultiplied = false;
if (encoder.palette_.Size() > 3 && effort > 0) {
WP2_CHECK_ALLOC_OK(configs->resize(configs->size() + 1));
// Copy the previous config.
configs->back() = configs->at(configs->size() - 2);
// Try using a MoveToFrontCache for images with more than 3 colors.
configs->back().group4_use_move_to_front = true;
configs->back().use_premultiplied = false;
}
}
// If the encoding algorithm is forced, only keep the corresponding configs.
if (encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kGroup4) {
for (size_t i = 0; i < configs->size();) {
if ((*configs)[i].algorithm != encoder.config_.lossless_algorithm) {
configs->erase(configs->begin() + i);
} else {
++i;
}
}
if (!configs->empty()) return WP2_STATUS_OK;
// TODO(vrabaud): we could decide to return an error if no configuration is
// found.
}
// The rest is for WebP algorithm.
if (encoder.config_.lossless_algorithm == WP2L::EncodingAlgorithm::kWebP) {
configs->clear();
}
WP2::Vector<EntropyConfig> entropy_configs;
WP2_CHECK_ALLOC_OK(entropy_configs.reserve(kPossibleEncodingRecipesNum));
if (effort == 0) {
if (configs->size() != 1) {
// AnalyzeEntropy is somewhat slow. 'red_and_blue_always_zero' is unused
// for effort == 0.
configs->clear();
WP2_CHECK_STATUS(
AddWebPConfig(can_use_palette ? EntropyMode::kPalette
: EntropyMode::kSpatialSubGreen,
can_premultiply, /*red_and_blue_always_zero=*/false,
/*entropy=*/0.f, effort, entropy_configs));
}
n_lz77s = 1;
} else {
// Try out multiple LZ77 on images with few colors.
n_lz77s = (can_use_palette && encoder.palette_.Size() <= 16) ? 2 : 1;
// For effort 5, we ask for 2 so that in case the best one is Palette, we
// still try a normal configuration because color cache can sometimes be
// better than palette.
const uint32_t num_configs_max = (effort < 5) ? 1
: (effort == 5) ? 2
: (effort > 5 && effort < 9)
? effort - 4
: std::numeric_limits<uint32_t>::max();
// TODO(vrabaud) use proper bits for analysis: we use can_use_palette even
// though we do not use it for most transforms like spatial.
// Using it instead of setting it to false gives 0.1% better
// compression ...
const uint32_t histo_bits =
GetHistoBits(effort, can_use_palette, pic.width(), pic.height());
const uint32_t transform_bits = GetTransformBits(effort, histo_bits);
WP2_CHECK_STATUS(AnalyzeEntropy(can_use_palette, effort, transform_bits,
encoder, num_configs_max, entropy_configs));
if (effort == 5 && entropy_configs[0].mode != EntropyMode::kPalette) {
// When we have palette as first transform, we also check the second one
// as it can be better. If the first one is not palette, only focus on the
// first one.
entropy_configs.resize_no_check(1);
}
}
WP2_CHECK_STATUS(progress.AdvanceBy(1.0));
// Remove the internal info.
for (const EntropyConfig& c : entropy_configs) {
WP2_CHECK_ALLOC_OK(configs->push_back(c.config));
}
// Use the minimal-size method for palette storage by default.
uint32_t configs_size = configs->size();
for (uint32_t i = 0; i < configs_size; ++i) {
for (uint32_t t = 0; t < kPossibleTransformCombinationSize; ++t) {
if ((*configs)[i].transforms[t].type == TransformType::kColorIndexing) {
(*configs)[i].palette_sorting_type = Palette::Sorting::kMinimalSize;
if (effort == 9) {
// Try more methods for palette sorting.
WP2_CHECK_ALLOC_OK(configs->push_back((*configs)[i]));
configs->back().palette_sorting_type = Palette::Sorting::kLuminance;
WP2_CHECK_ALLOC_OK(configs->push_back((*configs)[i]));
configs->back().palette_sorting_type =
Palette::Sorting::kModifiedZeng;
}
break;
}
}
}
// Configs are ordered with the smallest entropies first to favor early exits
// later. Still, we slightly change the order so that increasingly including
// sets of transforms are clumped together to favor transform caching later.
for (auto iter = configs->begin(); iter != configs->end();) {
// Copy the current config: it is the best one so far (entropy-wise), and
// we are looking for other configs with transform sequences
// included/including its own ones (we have to copy the config as 'iter'
// might be incremented).
CrunchConfig config = *iter;
if (config.GetNumTransforms() == 0) {
++iter;
continue;
}
// Find configs with sets of transforms contained or containing the current
// ones with increasing size.
for (uint32_t i = 1; i <= kPossibleTransformCombinationSize; ++i) {
// Look at the following configurations.
for (auto iter_next = iter; iter_next != configs->end(); ++iter_next) {
if (iter_next->GetNumTransforms() == i &&
(config.ShouldBeCachedFor(*iter_next) ||
iter_next->ShouldBeCachedFor(config))) {
// If the set of transforms contains the currently studied one, have
// it be the new studied one.
if (i > config.GetNumTransforms()) config = *iter;
std::swap(*iter, *iter_next);
++iter;
break;
}
}
}
}
// TODO(vrabaud): replace the decision to be based on an actual estimate
// of entropy, or even spatial variance of entropy.
for (CrunchConfig& c : *configs) {
const bool use_palette =
(c.GetNumTransforms() > 0 &&
c.transforms[0].type == TransformType::kColorIndexing);
c.histo_bits = GetHistoBits(effort, use_palette, pic.width(), pic.height());
c.transform_bits = GetTransformBits(effort, c.histo_bits);
}
// Fill in the different LZ77s.
assert(n_lz77s <= kCrunchLZ77Max);
for (CrunchConfig& c : *configs) {
for (uint32_t i = 0; i < n_lz77s; ++i) {
c.lz77s_types_to_try[i] = (i == 0) ? kLZ77Standard | kLZ77RLE : kLZ77Box;
}
c.lz77s_types_to_try_size = n_lz77s;
}
// Add a last chance config that minimizes the range of the residuals by
// having no transforms.
WP2_CHECK_ALLOC_OK(configs->push_back(configs->back()));
for (auto& t : configs->back().transforms) t.type = TransformType::kNum;
configs->back().is_skippable = true;
return WP2_STATUS_OK;
}
bool ShouldComputePalette(const Encoder& encoder) {
if (encoder.config_.lossless_config) {
const auto& transforms = encoder.config_.lossless_config->transforms;
return std::any_of(transforms.begin(), transforms.end(), [](const auto& t) {
return t.type == TransformType::kColorIndexing;
});
} else {
return true;
}
}
} // namespace
// Figure out some configurations to try. If one is imposed by the encoder, use
// it.
WP2Status EncoderAnalyze(const WP2::ProgressRange& progress,
Encoder* const encoder,
WP2::Vector<CrunchConfig>* const configs) {
const WP2::ArgbBuffer& pic = encoder->pic_;
const int effort = encoder->config_.effort;
assert(!pic.IsEmpty());
const WP2::ProgressRange palette_progress(progress, 0.5);
const WP2::ProgressRange analysis_progress(progress, 0.5);
// Do not compute the palette if not asked for.
if (ShouldComputePalette(*encoder)) {
WP2_CHECK_STATUS(encoder->palette_.AnalyzeAndCreate(pic));
WP2_CHECK_STATUS(encoder->palette_.FindBestMethod(
pic, palette_progress, Palette::Sorting::kMinimalSize, effort,
encoder));
} else {
WP2_CHECK_STATUS(palette_progress.AdvanceBy(1.0));
}
// Use the imposed config if any.
if (encoder->config_.lossless_config) {
WP2_CHECK_ALLOC_OK(configs->resize(1));
(*configs)[0] = *encoder->config_.lossless_config;
} else {
WP2_CHECK_STATUS(
EncoderAnalyzeDefault(analysis_progress, *encoder, configs));
}
// Make sure all configs are valid.
for (CrunchConfig& config : *configs) {
uint32_t index;
WP2_CHECK_STATUS(FindEncodingRecipe(config, index));
if (config.algorithm == EncodingAlgorithm::kWebP) {
WP2_CHECK_OK(config.lz77s_types_to_try_size >= 1 &&
config.lz77s_types_to_try_size <= kCrunchLZ77Max,
WP2_STATUS_INVALID_PARAMETER);
}
// Verify some properties for some transforms.
for (const TransformHeader& transform : config.transforms) {
const TransformType type = transform.type;
if (type == TransformType::kPredictor ||
type == TransformType::kPredictorWSub ||
type == TransformType::kCrossColor) {
WP2_CHECK_OK(config.transform_bits >= WP2L::kTransformBitsMin &&
config.transform_bits <= WP2L::kTransformBitsMax,
WP2_STATUS_INVALID_PARAMETER);
WP2_CHECK_OK(config.histo_bits >= WP2L::kHistogramBitsMin &&
config.histo_bits <= WP2L::kHistogramBitsMax,
WP2_STATUS_INVALID_PARAMETER);
}
if (type == TransformType::kCrossColorGlobal) {
WP2_CHECK_OK(transform.cc_global_y_index >= 1 &&
transform.cc_global_y_index <= 9,
WP2_STATUS_INVALID_PARAMETER);
WP2_CHECK_OK(transform.cc_global_uv_index >= 1 &&
transform.cc_global_uv_index <= 12,
WP2_STATUS_INVALID_PARAMETER);
}
// Make sure the palette is not empty if we have a color indexing
// transform.
if (type == TransformType::kColorIndexing) {
WP2_CHECK_OK(encoder->palette_.Size() > 0,
WP2_STATUS_INVALID_PARAMETER);
}
}
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
} // namespace WP2L