blob: 6f939dfe04b5eacf0c1da917fdced4da0b94b023 [file]
// Copyright 2020 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.
// -----------------------------------------------------------------------------
//
// Screen content encoding (e.g. screenshots)
//
// Author: Maryla (maryla@google.com)
#include "src/enc/screen_content/screen_enc.h"
#include <algorithm>
#include <cassert>
#include <cstdint>
#if !defined(WP2_BITTRACE)
#include <iostream>
#endif
#include "src/common/constants.h"
#include "src/common/progress_watcher.h"
#include "src/dsp/dsp.h"
#include "src/dsp/math.h"
#include "src/enc/analysis.h"
#include "src/enc/anim/anim_enc.h"
#include "src/enc/preview/preview_enc.h"
#include "src/enc/wp2_enc_i.h"
#include "src/utils/plane.h"
#include "src/utils/utils.h"
#include "src/wp2/base.h"
#include "src/wp2/debug.h"
#include "src/wp2/decode.h"
#include "src/wp2/encode.h"
#include "src/wp2/format_constants.h"
namespace WP2 {
namespace {
// Split the image into a grid of kScreenBlockSize x kScreenBlockSize blocks,
// and decide between lossy and lossless for each block.
// TODO(maryla): we could try using the real partitioning so the blocks follow
// image edges.
static constexpr uint32_t kScreenBlockSize = 32;
uint32_t GetDisto(const ArgbBuffer& left, const ArgbBuffer& right,
const Rectangle& window) {
uint64_t disto_per_channel[4] = {0, 0, 0, 0};
for (uint32_t y = 0; y < window.height; ++y) {
const uint8_t* const ptr1 = left.GetRow8(window.y + y);
const uint8_t* const ptr2 = right.GetRow8(window.y + y);
WP2SumSquaredError4x8u(&ptr1[window.x * 4], &ptr2[window.x * 4],
window.width, disto_per_channel);
}
return disto_per_channel[0] + disto_per_channel[1] + disto_per_channel[2] +
disto_per_channel[3];
}
bool HasOnlyOneColor(const ArgbBuffer& input, uint32_t i, uint32_t j) {
const uint32_t first_pix =
((uint32_t*)input.GetRow(j * kScreenBlockSize))[i * kScreenBlockSize];
for (uint32_t y = 0; y < kScreenBlockSize; ++y) {
if (j * kScreenBlockSize + y >= input.height()) break;
const uint32_t* const row =
(uint32_t*)input.GetRow(j * kScreenBlockSize + y);
for (uint32_t x = 0; x < kScreenBlockSize; ++x) {
if (i * kScreenBlockSize + x >= input.width()) break;
const uint32_t pix = row[i * kScreenBlockSize + x];
if (pix != first_pix) return false;
}
}
return true;
}
// Computes RD score per pixel.
WP2Status GetScores(const EncoderConfig& config, const ArgbBuffer& input,
float lambda, Planef* const scores) {
MemoryWriter memory_writer;
WP2_CHECK_STATUS(Encode(input, &memory_writer, config));
ArgbBuffer output;
DecoderConfig dec_config;
DecoderInfo info;
dec_config.info = &info;
Planef bits_per_pixel;
info.bits_per_pixel = &bits_per_pixel;
WP2_CHECK_STATUS(
Decode(memory_writer.mem_, memory_writer.size_, &output, dec_config));
const uint32_t block_height = DivCeil(input.height(), kScreenBlockSize);
const uint32_t block_width = DivCeil(input.width(), kScreenBlockSize);
WP2_CHECK_STATUS(scores->Resize(block_width, block_height));
scores->Fill(0.f);
for (uint32_t y = 0; y < block_height; ++y) {
for (uint32_t x = 0; x < block_width; ++x) {
const Rectangle window{
x * kScreenBlockSize, y * kScreenBlockSize,
std::min(kScreenBlockSize, input.width() - x * kScreenBlockSize),
std::min(kScreenBlockSize, input.height() - y * kScreenBlockSize)};
for (uint32_t j = 0; j < window.height; ++j) {
for (uint32_t i = 0; i < window.width; i++) {
scores->At(x, y) += bits_per_pixel.At(window.x + i, window.y + j);
}
}
const uint32_t disto = GetDisto(output, input, window);
const float score = scores->At(x, y) * lambda + disto;
scores->At(x, y) = score;
}
}
return WP2_STATUS_OK;
}
WP2Status DecideLossy(const ArgbBuffer& input, const Planef& lossy_scores,
const Planef& lossless_scores,
Plane<bool>* const is_lossy) {
const uint32_t block_width = lossy_scores.w_;
const uint32_t block_height = lossy_scores.h_;
WP2_CHECK_STATUS(is_lossy->Resize(block_width, block_height));
// Choose between lossy and lossless based on score.
for (uint32_t y = 0; y < block_height; ++y) {
for (uint32_t x = 0; x < block_width; ++x) {
if (HasOnlyOneColor(input, x, y)) {
// For monochrome areas, use the same mode as the neighboring blocks.
is_lossy->At(x, y) = (x > 0) ? is_lossy->At(x - 1, y)
: (y > 0) ? is_lossy->At(x, y - 1)
: false;
} else {
is_lossy->At(x, y) = (lossy_scores.At(x, y) < lossless_scores.At(x, y));
}
}
}
// Second step: remove small holes. If at least 3 direct neighbours share the
// same mode (lossy or lossless), the current block gets set to that mode.
bool something_changed;
uint16_t iter = 0;
const uint16_t kMaxIter = 5;
do {
something_changed = false;
for (uint32_t y = 0; y < block_height; ++y) {
for (uint32_t x = 0; x < block_width; ++x) {
uint16_t num_total = 0;
uint16_t num_lossy = 0;
if (x > 0) {
++num_total;
num_lossy += is_lossy->At(x - 1, y);
}
if (x + 1 < block_width) {
++num_total;
num_lossy += is_lossy->At(x + 1, y);
}
if (y > 0) {
++num_total;
num_lossy += is_lossy->At(x, y - 1);
}
if (y + 1 < block_height) {
++num_total;
num_lossy += is_lossy->At(x, y + 1);
}
const uint16_t num_lossless = num_total - num_lossy;
if (!is_lossy->At(x, y) &&
((num_total >= 3 && num_lossless <= 1) || (num_lossless == 0))) {
is_lossy->At(x, y) = true;
something_changed = true;
} else if (is_lossy->At(x, y) &&
((num_total >= 3 && num_lossy <= 1) || (num_lossy == 0))) {
is_lossy->At(x, y) = false;
something_changed = true;
}
}
}
} while (something_changed && ++iter < kMaxIter);
return WP2_STATUS_OK;
}
Rectangle ScaleRect(const Rectangle& rect, uint32_t multiplier) {
return {rect.x * multiplier, rect.y * multiplier, rect.width * multiplier,
rect.height * multiplier};
}
EncoderConfig LossyConfig(const EncoderConfig& base_config) {
assert(base_config.quality <= kMaxLossyQuality);
EncoderConfig config = base_config;
config.tile_shape = FinalTileShape(base_config);
return config;
}
EncoderConfig LosslessConfig(const EncoderConfig& base_config) {
const uint32_t kNearLosslessQuality = 98;
EncoderConfig config = base_config;
config.quality = kNearLosslessQuality;
config.tile_shape = FinalTileShape(base_config);
return config;
}
} // namespace
WP2Status EncodeScreenContent(const ArgbBuffer& input, Writer* const output,
const EncoderConfig& config) {
#if !defined(WP2_BITTRACE)
std::cerr << "WARNING! WP2_BITTRACE required for screen content\n";
return Encode(input, output, config);
#endif
if (config.quality > kMaxLossyQuality || config.quality <= 50) {
return Encode(input, output, config);
}
ProgressWatcher progress(config.progress_hook);
WP2_CHECK_STATUS(progress.Start());
// Empirical value. TODO(maryla): tune.
const float lambda = std::max(85.f, -42.f * config.quality + 3400.f);
Planef lossless_scores;
WP2_CHECK_STATUS(
GetScores(LosslessConfig(config), input, lambda, &lossless_scores));
// We don't actually need to keep lossy and lossless_score, we could subtract
// one from the other directly. But this is simpler/clearer.
Planef lossy_scores;
WP2_CHECK_STATUS(
GetScores(LossyConfig(config), input, lambda, &lossy_scores));
Plane<bool> is_lossy;
WP2_CHECK_STATUS(
DecideLossy(input, lossy_scores, lossless_scores, &is_lossy));
return EncodeScreenContent(input, is_lossy, kScreenBlockSize, output, config);
}
WP2Status EncodeScreenContent(const ArgbBuffer& input,
const Plane<bool>& is_lossy, uint32_t block_size,
Writer* const output,
const EncoderConfig& config) {
ProgressWatcher progress(config.progress_hook);
WP2_CHECK_STATUS(progress.Start());
const uint32_t block_height = DivCeil(input.height(), block_size);
const uint32_t block_width = DivCeil(input.width(), block_size);
RectangleGroup lossy_rect_group(1);
RectangleGroup lossless_rect_group(1);
for (uint32_t y = 0; y < block_height; ++y) {
for (uint32_t x = 0; x < block_width; ++x) {
if (is_lossy.At(x, y)) {
lossy_rect_group.AddPoint(x, y);
} else {
lossless_rect_group.AddPoint(x, y);
}
}
}
if (lossy_rect_group.GetNumRectangles() == 0 ||
lossless_rect_group.GetNumRectangles() == 0) {
if (lossless_rect_group.GetNumRectangles() > 0) {
return Encode(input, output, LosslessConfig(config));
}
return Encode(input, output, LossyConfig(config));
}
Subframe lossless_subframe;
Subframe lossy_subframe;
WP2_CHECK_STATUS(lossy_subframe.rgb_pixels.ConvertFrom(input));
// There is no need to set the format to WP2_ARGB_32 if the input is not
// premultiplied because the lossy subframe will introduce some loss anyway.
// TODO(yguyon): Handle Argb38
WP2_CHECK_STATUS(lossless_subframe.rgb_pixels.ConvertFrom(input));
lossy_subframe.window =
ScaleRect(lossy_rect_group.GetRectangle(0), block_size)
.ClipWith(lossy_subframe.GetFrameRect());
lossless_subframe.window =
ScaleRect(lossless_rect_group.GetRectangle(0), block_size)
.ClipWith(lossless_subframe.GetFrameRect());
lossless_subframe.duration_ms = 0;
lossless_subframe.dispose = true;
lossy_subframe.duration_ms = 1;
lossy_subframe.blend = true;
// Remove lossy areas from the lossless frame and vice versa.
for (uint32_t y = 0; y < block_height; ++y) {
for (uint32_t x = 0; x < block_width; ++x) {
const bool lossy = is_lossy.At(x, y);
const uint32_t w = std::min(input.width() - x * block_size, block_size);
for (uint32_t j = 0; j < block_size; ++j) {
if (y * block_size + j >= input.height()) break;
uint8_t* const lossless_row =
lossless_subframe.rgb_pixels.GetRow8(y * block_size + j);
uint8_t* const lossy_row =
lossy_subframe.rgb_pixels.GetRow8(y * block_size + j);
if (lossy) {
std::fill(&lossless_row[(x * block_size) * 4],
&lossless_row[(x * block_size + w) * 4], 0);
} else {
std::fill(&lossy_row[(x * block_size) * 4],
&lossy_row[(x * block_size + w) * 4], 0);
}
}
}
}
const bool has_alpha = true;
const bool is_premultiplied = DecidePremultiplied(input.format(), config);
const bool has_icc = (input.metadata_.iccp.size > 0);
const bool has_trailing_data =
(input.metadata_.xmp.size > 0) || (input.metadata_.exif.size > 0);
WP2_CHECK_STATUS(
EncodeHeader(config, input.width(), input.height(),
WP2Formatbpc(lossless_subframe.rgb_pixels.format()),
has_alpha, is_premultiplied, /*is_anim=*/true,
/*loop_forever=*/false, kDefaultBackgroundColor,
GetPreviewColor(input), has_icc, has_trailing_data, output));
WP2_CHECK_STATUS(progress.AdvanceBy(kOnePercent));
const ProgressRange preview_progress(&progress, kOnePercent);
WP2_CHECK_STATUS(EncodePreview(input, config, preview_progress, output));
WP2_CHECK_STATUS(EncodeICC(
{input.metadata_.iccp.bytes, input.metadata_.iccp.size}, output));
WP2_CHECK_STATUS(progress.AdvanceBy(kOnePercent));
const ProgressRange lossless_progress(
&progress, GetFrameProgression(/*frame_index=*/0, /*is_last=*/false));
const ProgressRange lossy_progress(
&progress, GetFrameProgression(/*frame_index=*/1, /*is_last=*/true));
SubframeEncoder frame_encode(has_alpha, is_premultiplied,
FinalTileShape(config));
WP2_CHECK_STATUS(frame_encode.EncodeSubframe(
LosslessConfig(config), lossless_subframe,
/*is_last=*/false, lossless_progress, output));
WP2_CHECK_STATUS(
frame_encode.EncodeSubframe(LossyConfig(config), lossy_subframe,
/*is_last=*/true, lossy_progress, output));
WP2_CHECK_STATUS(EncodeMetadata(input.metadata_, output));
WP2_CHECK_STATUS(progress.Finish());
return WP2_STATUS_OK;
}
} // namespace WP2