blob: 723f9ba8e8201bda22dbabf1dcb7cfe83de7d7ce [file]
// 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.
// -----------------------------------------------------------------------------
//
// Preview encoder
//
// Author: Skal (pascal.massimino@gmail.com)
#include "./preview_enc.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include "src/common/color_precision.h"
#include "src/common/preview/preview.h"
#include "src/common/progress_watcher.h"
#include "src/dsp/math.h"
#include "src/utils/ans.h"
#include "src/utils/ans_enc.h"
#include "src/utils/ans_utils.h"
#include "src/utils/utils.h"
#include "src/utils/vector.h"
#include "src/wp2/base.h"
namespace WP2 {
namespace {
//------------------------------------------------------------------------------
constexpr uint32_t AbsUInt32(uint8_t a, uint8_t b) {
return (uint32_t)((a >= b) ? (a - b) : (b - a));
}
void BlurPixel(const ArgbBuffer& src, uint32_t radius, uint32_t pixel_x,
uint32_t pixel_y, uint32_t threshold, ArgbBuffer* const dst) {
const Argb32b color = GetAverageColor(src, radius, pixel_x, pixel_y);
assert(src.format() == WP2_Argb_32 && src.format() == dst->format());
const uint8_t* const src_pixel = src.GetRow8(pixel_y) + pixel_x * 4;
uint8_t* const dst_pixel = dst->GetRow8(pixel_y) + pixel_x * 4;
if (AbsUInt32(src_pixel[0], color.a) < threshold) dst_pixel[0] = color.a;
if (AbsUInt32(src_pixel[1], color.r) < threshold) dst_pixel[1] = color.r;
if (AbsUInt32(src_pixel[2], color.g) < threshold) dst_pixel[2] = color.g;
if (AbsUInt32(src_pixel[3], color.b) < threshold) dst_pixel[3] = color.b;
}
WP2Status Blur(ArgbBuffer* const buffer, uint32_t radius, uint32_t threshold) {
if (radius < 1) return WP2_STATUS_OK;
ArgbBuffer original(buffer->format());
WP2_CHECK_STATUS(original.CopyFrom(*buffer));
for (uint32_t y = 0; y < buffer->height(); ++y) {
for (uint32_t x = 0; x < buffer->width(); ++x) {
BlurPixel(original, radius, x, y, threshold, buffer);
}
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
// 2x Horizontal averaging.
// 'canvas_view' must be a view on 'canvas'. Downscales the data in 'canvas' in
// place and updates canvas_view to the new dimensions.
bool HalfWidthDownscale(ArgbBuffer* const canvas, ArgbBuffer* const canvas_view,
uint32_t min_w) {
assert(canvas->format() == WP2_Argb_32);
assert(min_w > 1);
const uint32_t half_width = std::max(1u, canvas_view->width() / 2);
if (half_width < min_w) return true;
const uint32_t kChannels = 4;
for (uint32_t y = 0; y < canvas_view->height(); ++y) {
const uint8_t* src = canvas->GetRow8(y);
uint8_t* dst = canvas->GetRow8(y);
for (uint32_t x = 0; x < half_width; ++x) {
for (uint32_t c = 0; c < kChannels; ++c) {
dst[c] = RightShiftRound(src[c] + src[c + kChannels], 1);
}
src += 2 * kChannels;
dst += kChannels;
}
}
const WP2Status status =
canvas_view->SetView(*canvas, {0, 0, half_width, canvas_view->height()});
// No memory allocation so this shouldn't fail unless there is a bug.
(void)status;
assert(status == WP2_STATUS_OK);
return false;
}
// 2x Vertical averaging.
// 'canvas_view' must be a view on 'canvas'. Downscales the data in 'canvas' in
// place and updates canvas_view to the new dimensions.
bool HalfHeightDownscale(ArgbBuffer* const canvas,
ArgbBuffer* const canvas_view, uint32_t min_h) {
assert(canvas->format() == WP2_Argb_32);
assert(min_h > 1);
const uint32_t half_height = std::max(1u, canvas_view->height() / 2);
if (half_height < min_h) return true;
const uint32_t kChannels = 4;
for (uint32_t y = 0; y < half_height; ++y) {
const uint8_t* const src1 = canvas->GetRow8(2 * y + 0);
const uint8_t* const src2 = canvas->GetRow8(2 * y + 1);
uint8_t* const dst = canvas->GetRow8(y);
for (uint32_t x = 0; x < kChannels * canvas_view->width(); ++x) {
dst[x] = RightShiftRound(src1[x] + src2[x], 1);
}
}
const WP2Status status =
canvas_view->SetView(*canvas, {0, 0, canvas_view->width(), half_height});
// No memory allocation so this shouldn't fail unless there is a bug.
(void)status;
assert(status == WP2_STATUS_OK);
return false;
}
//------------------------------------------------------------------------------
// Encode the 'value' with 'stats'.
void PutAValue(ANSEnc* enc, int32_t value, ValueStats* const stats,
bool is_positive, WP2_OPT_LABEL) {
assert(!is_positive || value >= 0);
assert((uint32_t)std::abs(value) <= AYCoCg19b::kYCoCgMax);
if (enc->PutABit((value == 0) ? 1 : 0, &stats->zero, label)) return;
const bool is_negative = value < 0;
const uint32_t abs_value = (uint32_t)std::abs(value) - 1;
for (uint32_t i = 0, j = 1; j <= AYCoCg19b::kYCoCgMax; j <<= 1) {
assert(i < AYCoCg19b::kYCoCgBitDepth);
enc->PutABit((abs_value & j) ? 1 : 0, &stats->bits[i++], label);
}
if (!is_positive) enc->PutABit(is_negative ? 1 : 0, &stats->sign, label);
}
} // namespace
//------------------------------------------------------------------------------
// I/O
void PreviewData::EncodeColorIndex(uint16_t idx, ANSEnc* const enc,
ANSBinSymbol* const stats,
uint16_t* const prediction) const {
assert(idx < palette_.size());
const bool color_match = (idx == *prediction);
if (enc->PutABit(!color_match, stats, "color_index_match")) {
const uint32_t coded_idx = (idx < *prediction) ? idx : idx - 1u;
enc->PutRValue(coded_idx, palette_.size() - 1, "residual_color_index");
*prediction = idx;
}
}
WP2Status PreviewData::EncodePalette(ANSEnc* const enc) const {
WP2_CHECK_OK(palette_.size() >= kPreviewMinNumColors &&
palette_.size() <= kPreviewMaxNumColors,
WP2_STATUS_INVALID_PARAMETER);
ANSDebugPrefix prefix(enc, "palette");
enc->PutRange(palette_.size(), kPreviewMinNumColors, kPreviewMaxNumColors,
"num_colors");
bool has_alpha = false;
for (uint32_t i = 0; i < palette_.size() && !has_alpha; ++i) {
if (!palette_[i].a) has_alpha = true;
}
enc->PutBit((uint32_t)has_alpha, kPreviewOpaqueProba, kPreviewTotalProba,
"has_alpha");
AYCoCg19b pred = kPreviewStartingColorPrediction;
ANSBinSymbol alpha(2, 2); // TODO(yguyon): why 2, 2
ValueStats stat_yco, stat_cg;
for (AYCoCg19b color : palette_) {
if (has_alpha) {
enc->PutABit((uint32_t)(color.a ^ pred.a), &alpha, "residual_alpha");
}
const int32_t y = (int32_t)color.y - pred.y;
const int32_t co = (int32_t)color.co - pred.co;
const int32_t cg = (int32_t)color.cg - pred.cg;
// The 'cg' value is assumed to be sorted in increasing order.
// TODO(yguyon): why sort by 'cg'?
PutAValue(enc, y, &stat_yco, /*is_positive=*/false, "residual_y");
PutAValue(enc, co, &stat_yco, /*is_positive=*/false, "residual_co");
PutAValue(enc, cg, &stat_cg, /*is_positive=*/true, "residual_cg");
pred = color;
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
WP2Status PreviewData::EncodeVertices(ANSEnc* const enc) const {
uint32_t num_grid_points_left = grid_width_ * grid_height_ - 4u;
const uint32_t min_num_vertices = std::max(
std::max((uint32_t)palette_.size(), 4u) - 4u, kPreviewMinNumVertices);
const uint32_t max_num_vertices =
std::min(kPreviewMaxNumVertices, num_grid_points_left);
WP2_CHECK_OK(vertices_.size() >= min_num_vertices &&
vertices_.size() <= max_num_vertices,
WP2_STATUS_INVALID_PARAMETER);
ANSDebugPrefix prefix(enc, "vertices");
enc->PutRange(vertices_.size(), min_num_vertices, max_num_vertices,
"num_vertices");
WP2_CHECK_OK(vertices_.size() <= num_grid_points_left,
WP2_STATUS_INVALID_PARAMETER);
// Encode corners (thus only color_index).
ANSBinSymbol stat_idx(2, 2); // TODO(yguyon): why 2, 2
uint16_t predictor = 0;
for (const auto c : corners_) EncodeColorIndex(c, enc, &stat_idx, &predictor);
if (vertices_.empty()) return WP2_STATUS_OK;
// Encode other points.
uint32_t vertex_index = 0;
for (uint32_t y = 0; y < grid_height_; ++y) {
for (uint32_t x = 0; x < grid_width_; ++x) {
if (IsCorner(x, y)) continue;
const VertexIndexedColor& vertex = vertices_[vertex_index];
const bool position_match = (vertex.y == y && vertex.x == x);
const uint32_t num_vertices_left = vertices_.size() - vertex_index;
if (enc->PutBit((uint32_t)position_match,
num_grid_points_left - num_vertices_left,
num_grid_points_left, "position_match")) {
EncodeColorIndex(vertex.color_index, enc, &stat_idx, &predictor);
if (++vertex_index == vertices_.size()) return WP2_STATUS_OK;
}
--num_grid_points_left;
}
}
return WP2_STATUS_INVALID_PARAMETER; // Vertices outside the grid.
}
//------------------------------------------------------------------------------
void PreviewData::SetColorIndex(int vtx_idx, uint32_t index) {
assert(vtx_idx >= -4 && vtx_idx < (int)vertices_.size());
assert(index < palette_.size());
if (vtx_idx >= 0) {
vertices_[vtx_idx].color_index = index;
} else {
corners_[-1 - vtx_idx] = index;
}
}
WP2Status PreviewData::SortPalette() {
struct OrderedColor {
AYCoCg19b color;
uint32_t old_index;
bool used_by_a_vertex;
bool operator<(const OrderedColor& other) const {
if (color.cg != other.color.cg) return (color.cg < other.color.cg);
if (color.co != other.color.co) return (color.co < other.color.co);
if (color.y != other.color.y) return (color.y < other.color.y);
return (color.a < other.color.a);
}
bool operator==(const OrderedColor& other) const {
return (color.a == other.color.a) && (color.y == other.color.y) &&
(color.co == other.color.co) && (color.cg == other.color.cg);
}
};
Vector<OrderedColor> ordered_palette;
WP2_CHECK_ALLOC_OK(ordered_palette.resize(palette_.size()));
for (uint32_t i = 0; i < ordered_palette.size(); ++i) {
ordered_palette[i].color = palette_[i];
ordered_palette[i].old_index = i;
ordered_palette[i].used_by_a_vertex = false;
}
// Keep only used colors.
for (const auto& vertex : vertices_) {
WP2_CHECK_OK(vertex.color_index < ordered_palette.size(),
WP2_STATUS_INVALID_PARAMETER);
ordered_palette[vertex.color_index].used_by_a_vertex = true;
}
for (const auto& v : corners_) ordered_palette[v].used_by_a_vertex = true;
ordered_palette.erase(
std::remove_if(
ordered_palette.begin(), ordered_palette.end(),
[](const OrderedColor& color) { return !color.used_by_a_vertex; }),
ordered_palette.end());
// Sort it for easier duplicate removal.
std::sort(ordered_palette.begin(), ordered_palette.end());
// Remember colors old indices.
Vector_u32 old_index_to_new_index;
WP2_CHECK_ALLOC_OK(old_index_to_new_index.resize(palette_.size()));
palette_.clear(); // No need to reserve.
const OrderedColor* previous_valid_color = nullptr;
for (const OrderedColor& ordered_color : ordered_palette) {
if (previous_valid_color != nullptr &&
ordered_color == *previous_valid_color) {
// Don't keep duplicates.
old_index_to_new_index[ordered_color.old_index] =
old_index_to_new_index[previous_valid_color->old_index];
} else {
old_index_to_new_index[ordered_color.old_index] = palette_.size();
WP2_CHECK_ALLOC_OK(
palette_.push_back(ordered_color.color, /*resize_if_needed=*/false));
previous_valid_color = &ordered_color;
}
}
// Map the vertices to the new colors.
for (VertexIndexedColor& vertex : vertices_) {
vertex.color_index = old_index_to_new_index[vertex.color_index];
}
for (auto& v : corners_) v = old_index_to_new_index[v];
WP2_CHECK_STATUS(MakePaletteValid(&palette_));
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
WP2Status PreviewData::CleanupVertices() {
std::sort(vertices_.begin(), vertices_.end());
uint32_t size = 0;
for (auto& v : vertices_) {
if (IsCorner(v.x, v.y) || IsOutside(v.x, v.y)) continue;
if (size > 0 && v.x == vertices_[size - 1].x &&
v.y == vertices_[size - 1].y)
continue;
std::swap(vertices_[size], v);
++size;
}
vertices_.resize_no_check(size);
return WP2_STATUS_OK;
}
WP2Status PreviewData::SortVertices() {
// Sort by 'y' then by 'x' for easier duplicate check.
std::sort(vertices_.begin(), vertices_.end());
// Verify integrity.
for (uint32_t i = 0; i < vertices_.size(); ++i) {
const VertexIndexedColor& vertex = vertices_[i];
WP2_CHECK_OK(!IsCorner(vertex.x, vertex.y), WP2_STATUS_INVALID_PARAMETER);
// Is it inside the grid?
WP2_CHECK_OK(!IsOutside(vertex.x, vertex.y), WP2_STATUS_INVALID_PARAMETER);
// Is it different from the previous one?
WP2_CHECK_OK(i == 0 || (vertex.x != vertices_[i - 1].x ||
vertex.y != vertices_[i - 1].y),
WP2_STATUS_INVALID_PARAMETER);
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
WP2Status PreviewData::Encode(ANSEnc* const enc) const {
WP2_CHECK_STATUS(const_cast<PreviewData*>(this)->SortVertices());
WP2_CHECK_STATUS(const_cast<PreviewData*>(this)->SortPalette());
{
ANSDebugPrefix prefix(enc, "preview");
assert(grid_width_ >= kPreviewMinGridSize);
assert(grid_height_ >= kPreviewMinGridSize);
enc->PutRange(grid_width_, kPreviewMinGridSize, kPreviewMaxGridSize,
"grid_dim");
enc->PutRange(grid_height_, kPreviewMinGridSize, kPreviewMaxGridSize,
"grid_dim");
enc->PutBit((uint32_t)use_noise_, kPreviewNoiseProba, kPreviewTotalProba,
"use_noise");
WP2_CHECK_STATUS(EncodePalette(enc));
WP2_CHECK_STATUS(EncodeVertices(enc));
}
WP2_CHECK_STATUS(enc->GetStatus());
WP2_CHECK_STATUS(enc->AssembleToBitstream(/*clear_tokens=*/true));
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
namespace {
// Adjust buffer size and grid size.
// buffer_view should be a view on buffer. buffer's data will be modified,
// and buffer_view updated to the new dimensions.
WP2Status AdjustDimensions(const PreviewConfig& config,
ArgbBuffer* const buffer,
ArgbBuffer* const buffer_view,
PreviewData* const preview) {
uint32_t buffer_w = buffer->width(), buffer_h = buffer->height();
uint32_t grid_w = config.grid_size, grid_h = config.grid_size;
if (buffer_w >= buffer_h) {
grid_h = DivRound(grid_w * buffer_h, buffer_w);
} else {
grid_w = DivRound(grid_h * buffer_w, buffer_h);
}
grid_w = std::min(grid_w, kPreviewMaxGridSize);
grid_h = std::min(grid_h, kPreviewMaxGridSize);
grid_w = std::max(kPreviewMinGridSize, std::min(grid_w, buffer_w / 2));
grid_h = std::max(kPreviewMinGridSize, std::min(grid_h, buffer_h / 2));
preview->grid_width_ = grid_w;
preview->grid_height_ = grid_h;
// printf("Canvas: %d,%d, grid:%d,%d ", buffer_w, buffer_h, grid_w, grid_h);
buffer_w = std::lround(grid_w * config.grid_density);
buffer_h = std::lround(grid_h * config.grid_density);
// Downscale by 2x in alternate direction until reaching the dimension
// just above the final one
// TODO(skal): we could do a last pass of refinement with a simple
// bilinear interpolation.
bool done = false;
while (!done) {
done = HalfWidthDownscale(buffer, buffer_view, buffer_w) ||
HalfHeightDownscale(buffer, buffer_view, buffer_h);
}
// printf("final Canvas: %d,%d\n", buffer->width, buffer->height);
return WP2_STATUS_OK;
}
} // namespace
WP2Status PreviewData::Generate(const ArgbBuffer& canvas,
const PreviewConfig& config,
const ProgressRange& progress) {
WP2_CHECK_OK((grid_width_ >= kPreviewMinGridSize &&
grid_width_ <= kPreviewMaxGridSize),
WP2_STATUS_INVALID_PARAMETER);
WP2_CHECK_OK((grid_height_ >= kPreviewMinGridSize &&
grid_height_ <= kPreviewMaxGridSize),
WP2_STATUS_INVALID_PARAMETER);
const ProgressRange collect_progress(progress, 0.1);
const ProgressRange optimize_progress(progress, 0.9);
const uint32_t max_num_vertices =
std::min(config.num_vertices, grid_width_ * grid_height_ - 4);
// Find best vertices.
if (config.analysis_method ==
PreviewConfig::AnalysisMethod::kEdgeSelectionAndRepulsion) {
WP2_CHECK_STATUS(CollectVerticesFromEdgeSelectionAndRepulsion(
canvas, max_num_vertices, this));
} else if (config.analysis_method ==
PreviewConfig::AnalysisMethod::kColorDiffMaximization) {
WP2_CHECK_STATUS(CollectVerticesFromColorDiffMaximization(
canvas, max_num_vertices, this));
} else if (config.analysis_method ==
PreviewConfig::AnalysisMethod::kRandomEdgeSelection) {
WP2_CHECK_STATUS(
CollectVerticesFromRandomEdgeSelection(canvas, max_num_vertices, this));
} else {
WP2_CHECK_STATUS(
CollectVerticesFromVoronoiCells(canvas, max_num_vertices, this));
}
WP2_CHECK_STATUS(CollectColors2(canvas, config.num_colors));
WP2_CHECK_STATUS(collect_progress.AdvanceBy(1.));
WP2_CHECK_STATUS(
Optimize(canvas, config, optimize_progress, /* log= */ false));
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
// Entry point
WP2Status EncodePreview(const ArgbBuffer& buffer, const PreviewConfig& config,
const ProgressRange& progress, Writer* const output) {
WP2_CHECK_OK(config.IsValid(), WP2_STATUS_INVALID_CONFIGURATION);
WP2_CHECK_OK(output != nullptr, WP2_STATUS_NULL_PARAMETER);
const ProgressRange blur_progress(progress, 0.1);
const ProgressRange generate_progress(progress, 0.8);
const ProgressRange encode_progress(progress, 0.1);
// Prepare canvas.
ArgbBuffer canvas(WP2_Argb_32);
WP2_CHECK_STATUS(canvas.ConvertFrom(buffer));
// View on 'canvas', so we can can resize it without reallocating data.
ArgbBuffer canvas_view(canvas.format());
WP2_CHECK_STATUS(canvas_view.SetView(canvas));
PreviewData preview;
WP2_CHECK_STATUS(AdjustDimensions(config, &canvas, &canvas_view, &preview));
WP2_CHECK_STATUS(
Blur(&canvas_view, config.blur_radius, config.blur_threshold));
WP2_CHECK_STATUS(blur_progress.AdvanceBy(1.));
// Generate and optimize.
WP2_CHECK_STATUS(preview.Generate(canvas_view, config, generate_progress));
// Create bitstream.
ANSEnc enc;
WP2_CHECK_STATUS(preview.Encode(&enc));
WP2_CHECK_STATUS(encode_progress.AdvanceBy(1.));
WP2_CHECK_STATUS(enc.WriteBitstreamTo(*output));
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
} // namespace WP2