blob: 33bf15dc27405f06eaadeffbe6ccdd0e1f7c673e [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.
// -----------------------------------------------------------------------------
//
// WP2 visualization tool
// usage: vwp2 input.png [-q quality][...]
//
// Author: Skal (pascal.massimino@gmail.com)
#if defined(__unix__) || defined(__CYGWIN__)
#define _POSIX_C_SOURCE 200112L // for setenv
#endif
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <numeric>
#include <string>
#include <vector>
#ifdef HAVE_CONFIG_H
#include "src/wp2/config.h"
#endif
#include "examples/example_utils.h"
#include "imageio/image_dec.h"
#include "imageio/image_enc.h"
#include "imageio/imageio_util.h"
#include "src/common/color_precision.h"
#include "src/common/constants.h"
#include "src/common/lossy/block_size.h"
#include "src/common/vdebug.h"
#include "src/dsp/dsp.h"
#include "src/dsp/math.h"
#include "src/enc/wp2_enc_i.h"
#include "src/utils/plane.h"
#include "src/utils/random.h"
#include "src/utils/utils.h"
#include "src/wp2/base.h"
#include "src/wp2/decode.h"
#include "src/wp2/encode.h"
#include "src/wp2/format_constants.h"
#if defined(WP2_HAVE_AOM)
#include "extras/aom_utils.h"
#endif
#if defined(WP2_HAVE_OPENGL) && !defined(WP2_REDUCE_BINARY_SIZE)
#if defined(WP2_HAVE_FREEGLUT)
#include <GL/freeglut.h>
#include <GL/freeglut_ext.h>
#else
#define GL_SILENCE_DEPRECATION // to avoid deprecation warnings on MacOS
#if defined(HAVE_GLUT_GLUT_H)
#include <OpenGL/gl.h>
#include <GLUT/glut.h>
#else
#include <GL/gl.h>
#include <GL/glut.h>
#endif
#endif // !WP2_HAVE_FREEGLUT
#if defined(_MSC_VER) && _MSC_VER < 1900
#define snprintf _snprintf
#endif
namespace WP2 {
namespace {
// Scroll wheel events also sends mouse click events as button 3 and 4 (which
// don't seem to have a GLUT constants).
constexpr int kScrollWheelUp = 3;
constexpr int kScrollWheelDown = 4;
//------------------------------------------------------------------------------
// T must be cast-able to and from 'int'
template <class T>
void ModifyInRange(T& value, int max, int increment) {
int v = (int)value + increment;
v = (v + max) % max; // deals with negative values
value = (T)v;
}
template <class T>
void Modify(T& value, int max, bool increment, int increment_step = 1) {
ModifyInRange(value, max, increment ? increment_step : -increment_step);
}
// For ranges
template <class T>
void ModifyR(T& value, T max, bool increment, int increment_step = 1) {
if (glutGetModifiers() == GLUT_ACTIVE_ALT) {
// Toggle on/off (GLUT doesn't handle shift+alt+key: can't use 'increment')
value = (T)(((int)value > (int)max / 2) ? 0 : (int)max - 1);
} else {
Modify(value, max, increment, increment_step);
}
}
//------------------------------------------------------------------------------
// Visual Debug: set DecoderInfo::visual_debug to get extra visual data into
// DecoderInfo::debug_output during decoding.
struct VDToken {
VDToken(const char* const token_name, // NOLINT - not explicit
std::vector<VDToken>&& next_tokens = std::vector<VDToken>())
: name(token_name),
next(std::move(next_tokens)),
longest_next_name_size(
next.empty() ? 0
: (int32_t)std::max_element(
next.begin(), next.end(),
[](const VDToken& lhs, const VDToken& rhs) {
return lhs.name.size() < rhs.name.size();
})->name.size()) {}
const std::string name;
const std::vector<VDToken> next; // Direct children.
const int32_t longest_next_name_size; // Longest name size amongst 'next'.
};
std::vector<VDToken> GetVDTokensCfl() {
return {"best-prediction", "best-residuals", "best-slope", "best-intercept"};
}
std::vector<VDToken> GetVDTokensChannelCommon() {
return {{"compressed"},
{"prediction", {{"raw"}, {"modes", {"long", "short"}}}},
{"residuals"},
{"encoder", {"original-residuals", "prediction-scores"}}};
}
std::vector<VDToken> GetVDTokensAlpha() {
std::vector<VDToken> r = GetVDTokensChannelCommon();
r.emplace_back<VDToken>({"is_lossy"});
r.emplace_back<VDToken>({"lossy"});
r.emplace_back<VDToken>({"lossless"});
return r;
}
std::vector<VDToken> GetVDTokensY() {
std::vector<VDToken> r = GetVDTokensChannelCommon();
r.emplace_back("coeff-method");
r.emplace_back("has-coeffs");
return r;
}
std::vector<VDToken> GetVDTokensUV() {
std::vector<VDToken> r = GetVDTokensY();
r.emplace_back<VDToken>(
{"chroma-from-luma", {"prediction", "slope", "intercept"}});
return r;
}
// Tree of possible 'visual_debug' paths.
const VDToken kVisualDebug // NOLINT - non-trivially destructible global var
{"",
{{"decompressed"}, // No vdebug.
{"blocks",
{{"partition", {"split-tf", "blocks-only"}},
"segment-ids",
{"quantization", {"y", "u", "v", "a"}},
"is420",
{"encoder", {"is420-scores", "lambda-mult"}}}},
{"transform", {"xy", "x", "y"}},
{"y", GetVDTokensY()},
{"u", GetVDTokensUV()},
{"v", GetVDTokensUV()},
{"a", GetVDTokensAlpha()},
{"filters",
{{"filter-block-map", {{"bpp"}, {"res", {"y", "u", "v", "a"}}}},
{"deblocking-filter",
{{"diff", {"yuv", "a"}},
{"strength",
{{"horizontal", {"y", "u", "v", "a"}},
{"vertical", {"y", "u", "v", "a"}}}}}},
{"directional-filter", {"diff", "strength", "direction", "variance"}},
{"restoration-filter", {"diff", "strength"}},
{"intertile-filter",
{"diff",
{"strength",
{{"horizontal", {"y", "u", "v", "a"}},
{"vertical", {"y", "u", "v", "a"}}}}}},
{"grain-filter", {"diff"}},
{"alpha-filter", {"diff"}}}},
{"encoder",
{{"partition",
{"method",
{"pass",
{"all", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19"}},
"order",
"score",
{"multi",
{{"luma-alpha-gradient", {"32x32", "16x16", "8x8", "4x4"}},
{"narrow-std-dev", {"32x32", "16x16", "8x8", "4x4"}},
{"direction", {"32x32", "16x16", "8x8", "4x4"}},
{"analysis", {"direction-4x4"}}}}}},
{"segmentation", {"variance", "score", "id", "blocks"}},
{"grain",
{{"y", {"strength", "freq"}},
{"segments_y", {"strength", "freq"}},
{"u", {"strength", "freq"}},
{"v", {"strength", "freq"}},
{"segments_uv", {"strength", "freq"}}}},
{"error-diffusion",
{{"u", {"propagated-error", "new-error"}},
{"v", {"propagated-error", "new-error"}}}},
{"chroma-from-luma",
{{"u", GetVDTokensCfl()},
{"v", GetVDTokensCfl()},
{"a", GetVDTokensCfl()}}}}},
{"compressed", {"a", "r", "g", "b"}},
// Requires the reference image to be copied to DecoderInfo::debug_output.
{"original",
{"diff",
"a",
"r",
"g",
"b",
"y",
"u",
"v",
{"histogram", {"y", "u", "v"}}}},
{"lossless",
{{"symbols",
"clusters",
{"transformed", {"0", "1", "2"}},
{"prediction", {"reference", "raw", "modes"}},
{"cross-color", {"raw", "transform"}}}}},
// Requires WP2_BITTRACE to be defined during the compilation.
{"bits-per-pixel", {{"overall"}, {"coeffs", {"y", "u", "v"}}}},
{"error-map",
{"PSNR",
"SSIM",
"MSSSIM",
"LSIM",
{"diff-with-alt", {"PSNR", "SSIM", "MSSSIM", "LSIM"}}}}}};
// Verifies that 'str' starts with 'token' and that the ending or the next
// character (if any) is the separator '/'.
bool StrStartsWithToken(const std::string& str, const std::string& token) {
return (std::strncmp(str.c_str(), token.c_str(), token.size()) == 0 &&
(str.size() <= token.size() || str[token.size()] == '/' ||
(!token.empty() && token.back() == '/')));
}
// Recursively retrieves the paths to all leaves.
void GetAllLeavesPaths(const VDToken& node, const std::string& current_path,
std::vector<std::string>* const paths) {
for (const VDToken& next : node.next) {
if (next.next.empty()) {
paths->push_back(current_path + next.name);
} else {
GetAllLeavesPaths(next, current_path + next.name + '/', paths);
}
}
}
// Returns the path to the first leaf matching 'mask' or to an adjacent one
// depending on 'leaf_offset'.
std::string GetLeaf(const std::string& mask, int32_t leaf_offset = 0) {
// Printing all paths to an array then parsing it is not great but simple.
std::vector<std::string> leaves_paths;
GetAllLeavesPaths(kVisualDebug, "", &leaves_paths);
for (uint32_t i = 0; i < leaves_paths.size(); ++i) {
if (StrStartsWithToken(leaves_paths[i], mask)) {
return leaves_paths[((int32_t)(i + leaves_paths.size()) + leaf_offset) %
leaves_paths.size()];
}
}
return "";
}
std::string ChangeChannel(const std::string& path, int increment_step = 1) {
Channel old_channel;
if (VDMatch(path.c_str(), "y")) {
old_channel = kYChannel;
} else if (VDMatch(path.c_str(), "u")) {
old_channel = kUChannel;
} else if (VDMatch(path.c_str(), "v")) {
old_channel = kVChannel;
} else if (VDMatch(path.c_str(), "a")) {
old_channel = kAChannel;
} else {
// Return path unchanged if it doesn't have a channel in it.
return path;
}
const char to_find = std::tolower(kChannelStr[(int)old_channel][0]);
uint32_t pos = 0;
while (pos < path.length()) {
if (path[pos] == to_find &&
((pos == path.length() - 1) || path[pos + 1] == '/')) {
break;
}
// Find next slash
while (pos < path.length() && path[pos] != '/') ++pos;
++pos; // Move to first character after slash.
}
assert(pos < path.length());
std::vector<std::string> leaves_paths;
GetAllLeavesPaths(kVisualDebug, "", &leaves_paths);
int new_channel = old_channel;
for (uint32_t i = 0; i < 3; ++i) { // At most 3 channels to try.
ModifyInRange(new_channel, 4, increment_step);
const char to_replace = std::tolower(kChannelStr[new_channel][0]);
std::string new_path = path;
new_path[pos] = to_replace;
// Check if this path exists.
if (std::find(leaves_paths.begin(), leaves_paths.end(), new_path) !=
leaves_paths.end()) {
return new_path;
}
}
return path;
}
//------------------------------------------------------------------------------
constexpr uint32_t kNumSparks = 1000;
// Unfortunate global variables. Gathered into a struct for comfort.
class Params {
public:
Params() : current_file_(~0u) {
enc_config_.thread_level = 64;
enc_config_.info = &einfo_;
dec_config_.thread_level = 64;
dec_config_.info = &dinfo_;
#if defined(WP2_BITTRACE)
einfo_.store_blocks = true; // Preemptively to avoid (slow) reencoding.
#endif
}
WP2_NO_DISCARD bool SetCurrentFile(uint32_t file_number);
WP2_NO_DISCARD bool SetBitstream(const char* const file_name);
WP2_NO_DISCARD bool SetAltFile(const char* const file_name);
WP2_NO_DISCARD bool SetAltImage();
WP2_NO_DISCARD bool DumpCurrentCanvas(const char* const file_path);
void ShiftAlt(); // move alt1 to alt2
void SetQuality(float incr);
WP2_NO_DISCARD bool EncodeImage();
WP2_NO_DISCARD bool DecodeOutput();
WP2_NO_DISCARD bool EncodeAndDecode();
WP2_NO_DISCARD bool DecodeAndSwap(ArgbBuffer* const buffer);
WP2_NO_DISCARD bool GetOriginal();
WP2_NO_DISCARD bool GetCompressed();
WP2_NO_DISCARD bool ComputeYUVDistortion();
const ArgbBuffer& GetBuffer() const;
WP2_NO_DISCARD bool GetCurrentCanvas(ArgbBuffer* const buffer);
WP2_NO_DISCARD bool EncodeWebP(bool match_size);
float webp_distortion_ = 0.f;
uint32_t webp_size_ = 0;
WP2_NO_DISCARD bool EncodeAV1(bool copy_partition, float av1_quality,
size_t* const av1_file_size = nullptr);
WP2_NO_DISCARD bool EncodeAV1ToMatch(bool copy_partition,
size_t target_file_size);
WP2Status status_ = WP2_STATUS_OK;
enum Show {
kDebug,
kOriginal,
kCompressed,
kPreview,
kPreviewColor,
kAlt1, kAlt2,
kWebP,
kInfo,
kHelp,
kMenu
};
Show show_ = kDebug;
int mouse_x_ = 20, mouse_y_ = 20; // In pixels, starting from top left.
int mouse_x_down_ = 20, mouse_y_down_ = 20; // Position at last GLUT_DOWN.
bool moved_since_last_down_ = false;
std::string message_;
int message_end_time_;
bool show_interface_ = true;
size_t last_msg_hash_ = 0;
// Converts from pixel sizes to relative sizes as used by OpenGL.
float ToRelativeX(int32_t px) const { return px * 2.f / viewport_width_; }
float ToRelativeY(int32_t px) const { return px * 2.f / viewport_height_; }
// Converts from pixel positions to absolute positions as used by OpenGL.
float ToAbsoluteX(int32_t px) const { return ToRelativeX(px) - 1.f; }
float ToAbsoluteY(int32_t px) const { return 1.f - ToRelativeY(px); }
// Converts from viewport pixel position to image pixel coordinates.
uint32_t ToImageX(int viewport_x) const;
uint32_t ToImageY(int viewport_y) const;
// Same as glRectf() but with 'rect' in pixels, top-left being (0, 0).
// Draws a line if width or height is 0.
void glRect(const Rectangle& rect, uint32_t thickness = 1) const;
float sparks_x_[kNumSparks] = {0};
float sparks_y_[kNumSparks] = {0};
uint32_t sparks_index_ = 0;
bool display_sparks_ = false;
void DisplaySparks();
enum {
kDisplayNone,
kDisplayHeader,
kDisplayYCoeffs,
kDisplayUCoeffs,
kDisplayVCoeffs,
kDisplayACoeffs,
kDisplayPredModes,
kDisplayGrid,
kDisplayNum
} display_block_ = kDisplayNone;
void DisplayBlockInfo(std::vector<std::string>* const msg);
// side-func to help DisplayBlockInfo()
void PrintBlockHeader(const BlockInfo& b,
std::vector<std::string>* const msg) const;
void PrintResiduals(const BlockInfo& b, const BlockInfo& b_enc,
std::vector<std::string>* const msg) const;
void PrintPredModes(const BlockInfo& b,
std::vector<std::string>* const msg) const;
const char* partition_file_path_ = "/tmp/partition.txt";
const char* dump_wp2_path_ = "/tmp/dump.wp2";
const char* dump_png_path_ = "/tmp/dump.png";
const char* dump_av1_path_ = "/tmp/dump.av1";
// If not empty, is the current block being drawn with the mouse.
Rectangle forcing_block_;
// Same as EncoderInfo::force_partition but not yet encoded.
std::vector<Rectangle> force_partition_;
std::vector<EncoderInfo::ForcedParam> force_param_;
void ClearForcedElements();
bool IsDebugViewVisible() const;
bool IsPartitionVisible() const;
bool IsSegmentVisible() const;
bool IsPredictorVisible() const;
bool IsTransformVisible() const;
void DisplayPartition() const;
void DisplayForcedSegments() const;
void DisplayForcedPredictors() const;
void DisplayForcedTransforms() const;
#if defined(WP2_BITTRACE)
// Displays a sort of bar graph at the bottom of the screen showing the
// relative size of different syntax elements.
// 'traces' contains the size in bits taken by each (group of) symbols.
void DisplayBitTraces() const;
#endif
uint32_t getLineThickness() const;
bool ReadAndConvertPartition(bool read_only = true);
// Checks if either the encoding or decoding visual debug config match the
// given token.
bool VDMatchEncDec(const char* token) const;
// Updates the current visual debug view.
bool SetVisualDebug(std::string visual_debug);
bool IsHamburgerMenuVisible() const;
void DisplayMenu();
bool DisplaySubMenu(int32_t x, int32_t y, const VDToken& prefix,
std::string* const current_selection);
// Adds a temporary 'message' on screen.
void SetMessage(const std::string& message, bool even_if_show_info = false,
int duration_ms = 2000);
// Same as above with different default arguments.
void AddInfo(const std::string& message);
void SetError(const std::string& message);
// Top-level window refresh.
void PrintInfo();
void PrintMessages(const std::vector<std::string>& msg, bool print_lower,
const float text_color[4], const float bg_color[4],
bool small = false, bool outline = false);
std::vector<std::string> GetHelp();
// Reshapes the window based on current image size and zoom level.
void ReshapeWindow();
void ApplyZoomLevel();
void UpdateViewZoomLevel(int incr);
// Event handling.
void HandleKey(unsigned char key, int pos_x, int pos_y);
void HandleKeyUp(unsigned char key, int pos_x, int pos_y);
void HandleMouseMove(int x, int y, bool button_pressed);
// Functions below return true if the click event was handled, false if
// nothing was done.
// Handles a click for the given param type.
#if defined(WP2_BITTRACE)
bool HandleParamTypeClick(EncoderInfo::ForcedParam::Type type,
uint32_t value_range, int button, int state,
uint32_t img_x_down, uint32_t img_y_down,
int incr = 1, Channel channel = kYChannel);
#endif
bool HandleSegmentClick(int button, int state, uint32_t img_x_down,
uint32_t img_y_down, int incr = 1);
bool HandlePredictorClick(int button, int state, uint32_t img_x_down,
uint32_t img_y_down, int incr);
bool HandleTransformClick(int button, int state, uint32_t img_x_down,
uint32_t img_y_down, int incr);
// Calls HandleSegmentClick, HandlePredictorClick and HandleTransformClick.
bool HandleForcedParamClick(int button, int state, uint32_t img_x_down,
uint32_t img_y_down, uint32_t incr);
bool HandlePartitionClick(int button, int state, uint32_t img_x_down,
uint32_t img_y_down, uint32_t img_x,
uint32_t img_y);
bool HandleMenuClick(int button, int state, uint32_t img_x_down,
uint32_t img_y_down, uint32_t img_x, uint32_t img_y);
void HandleMouseClick(int button, int state, int x, int y);
void HandleMouseWheel(bool is_up, int x, int y);
void HandleDisplay();
enum Background {
kCheckerboard,
kWhite,
kBlack,
kPink,
kOpaque, // no background is displayed, behaves as if alpha == kMaxAlpha
kBackgroundNum,
} background_ = kCheckerboard;
// Debug environment value. Can be retrieved with 'atoi(getenv("WP2DBG"))'.
uint32_t wp2dbg_value_ = 0;
size_t current_file_;
std::vector<std::string> files_;
std::string original_file_; // used with view_only_
std::string input_; // input file
std::string output_; // currently encoded file
float riskiness_ = 0.f;
uint32_t enc_duration_ms_; // encoding time in ms
uint32_t dec_duration_ms_; // decoding time in ms
float quality_ = 75.;
float alpha_quality_ = 100.;
EncoderConfig enc_config_; // encoding parameters
DecoderConfig dec_config_;
std::string menu_selection_ = "decompressed"; // hovered menu selection
std::string visual_debug_ = "decompressed"; // displayed debug view
std::string visual_debug_prev_ = "decompressed"; // last displayed debug view
bool view_only_ = false;
bool use_premultiplied_ = false; // original samples in ARGB
ArgbBuffer in_ = ArgbBuffer(use_premultiplied_ ? WP2_Argb_32 : WP2_ARGB_32);
ArgbBuffer in_yuv_; // original alpha, Y, U or V samples
// image shown on spacebar (can be a view on 'in' or 'in_yuv')
ArgbBuffer original_;
ArgbBuffer out_; // recompressed samples
ArgbBuffer out_yuv_; // recompressed samples
ArgbBuffer preview_;
Argb32b preview_color_;
ArgbBuffer webp_; // result of WebP compression
// alternate comparison pictures & messages
ArgbBuffer alt1_, alt2_;
std::string alt1_msg_, alt2_msg_;
EncoderInfo einfo_;
DecoderInfo dinfo_;
std::string original_selection_info_;
MetricType distortion_metric_ = PSNR;
float distortion_[5] = {0.f}, yuv_distortion_[3] = {0.f};
int bit_trace_ = 0;
uint32_t bit_trace_level_ = 0;
uint32_t visual_bit_trace_level_ = 3;
uint32_t width_, height_; // Pixel with/height of the image.
Rectangle view_rect_; // Part of the image being shown.
Rectangle view_rect_down_; // View rect when mouse was clicked. For dragging.
int view_zoom_level_ = 0; // Zoom level used to determine view_rect_.
// Display zoom level: 1 image pixel = 2^zoom_level pixels shown.
int zoom_level_ = 0;
// Size of viewport which is view_rect_.width/height * 2^zoom_level
uint32_t viewport_width_, viewport_height_;
bool crop_ = false;
Rectangle crop_area_;
bool keep_metadata_ = false;
};
// Made accessible to plain C function callbacks of glut.
Params* global_params = nullptr;
bool Params::SetVisualDebug(std::string visual_debug) {
assert(!visual_debug.empty());
visual_debug_prev_ = visual_debug_;
visual_debug_ = visual_debug;
dinfo_.visual_debug = visual_debug_.c_str();
einfo_.visual_debug = visual_debug_.c_str();
if (!VDMatch(enc_config_, "encoder")) {
einfo_.visual_debug = nullptr;
}
if (VDMatch(dec_config_, "decompressed") || VDMatch(dec_config_, "encoder")) {
// "decompressed" is in fact the regular output.
// "encoder" is reserved to EncoderConfig.
dinfo_.visual_debug = nullptr;
}
if (VDMatch(enc_config_, "encoder")) {
if (!EncodeAndDecode()) return false;
} else {
if (!DecodeOutput()) return false;
}
return true;
}
//------------------------------------------------------------------------------
// Drawing tools
// Displays 'text' starting at pixel (x, y) in OpenGL coordinates [-1:1].
void PrintString(float x, float y, const std::string& text,
bool small = false) {
glRasterPos2f(x, y);
void* const font = small ? GLUT_BITMAP_8_BY_13 : GLUT_BITMAP_9_BY_15;
for (char c : text) glutBitmapCharacter(font, c);
}
uint32_t Params::ToImageX(int viewport_x) const {
return view_rect_.x +
DivRound((uint32_t)Clamp<int>(viewport_x, 0, viewport_width_ - 1) *
(view_rect_.width - 1),
viewport_width_ - 1);
}
uint32_t Params::ToImageY(int viewport_y) const {
return view_rect_.y +
DivRound((uint32_t)Clamp<int>(viewport_y, 0, viewport_height_ - 1) *
(view_rect_.height - 1),
viewport_height_ - 1);
}
// Floors to block-aligned coordinate.
uint32_t AlignWithBlocks(uint32_t coord) {
return coord - coord % kMinBlockSizePix;
}
void Params::DisplaySparks() {
if (!display_sparks_) return;
static UniformIntDistribution random(/*seed=*/0u);
sparks_x_[sparks_index_] =
ToRelativeX(mouse_x_ + random.Get<int32_t>(-5, 5)) - 1.f;
sparks_y_[sparks_index_] = -(ToRelativeY(mouse_y_) - 1.f);
sparks_index_ = (sparks_index_ + 1) % kNumSparks;
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
for (uint32_t i = 0; i < kNumSparks; ++i) {
if (sparks_x_[i] != 0 && sparks_y_[i] != 0) {
sparks_y_[i] -= ToRelativeY(2);
glColor4f(random.Get<int32_t>(100, 255) / 255.f,
random.Get<int32_t>(100, 255) / 255.f,
random.Get<int32_t>(100, 255) / 255.f, 1.f);
glRectf(sparks_x_[i] - ToRelativeX(1), sparks_y_[i] - ToRelativeY(1),
sparks_x_[i] + ToRelativeX(1), sparks_y_[i] + ToRelativeY(1));
if (sparks_y_[i] < -1) {
sparks_x_[i] = sparks_y_[i] = 0;
}
}
}
}
//------------------------------------------------------------------------------
// Hamburger menu
constexpr int32_t kMenuCharWidth = 9;
constexpr int32_t kMenuItemHeight = 20;
constexpr int32_t kMenuBorderWidth = 2;
// Renders a submenu at (x, y) with children of 'prefix'. Highlights the node
// matching the 'current_selection' or sets it to the hovered entry.
// Returns true if the mouse at 'mouse_x, mouse_y' is inside the submenu.
bool Params::DisplaySubMenu(int32_t x, int32_t y, const VDToken& prefix,
std::string* const current_selection) {
const int32_t longest_name_size = prefix.longest_next_name_size;
const int32_t submenu_width =
(longest_name_size + 3) * kMenuCharWidth + 2 * kMenuBorderWidth;
const int32_t submenu_height = prefix.next.size() * kMenuItemHeight;
const int32_t left = x, right = x + submenu_width - 1; // Inclusive.
const bool mouse_is_nearby =
(mouse_x_ >= left && mouse_x_ <= right + 50 && mouse_y_ + 150 >= y &&
mouse_y_ <= y + submenu_height + 150);
const bool mouse_in_column = (mouse_is_nearby && mouse_x_ <= right);
const bool mouse_in_submenu =
(mouse_in_column && mouse_y_ >= y && mouse_y_ <= y + submenu_height - 1);
bool was_hovered = false; // True if an element is hovered by the mouse.
// Background.
const float kMenuAlpha = .8;
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glColor4f(0.f, 0.f, 0.f, kMenuAlpha);
glRectf(ToAbsoluteX(x), ToAbsoluteY(y),
ToAbsoluteX(x + submenu_width), ToAbsoluteY(y + submenu_height));
// For each branch in the current VisualDebug tree node.
int32_t top = y;
for (const VDToken& token : prefix.next) {
const int32_t bottom = top + kMenuItemHeight - 1; // Inclusive.
// Set 'current_selection' if hovered by the mouse.
const bool is_hovered =
(mouse_in_submenu && mouse_y_ >= top && mouse_y_ <= bottom);
if (is_hovered) {
assert(!was_hovered); // Check that only one item is hovered.
*current_selection = token.name;
if (!token.next.empty()) *current_selection += "/";
was_hovered = true;
}
// Do not set as selected if another entry in submenu is hovered.
bool is_selected =
(is_hovered || (!mouse_in_column &&
StrStartsWithToken(*current_selection, token.name)));
std::string displayed_name = token.name;
if (!token.next.empty()) {
displayed_name.append(longest_name_size - token.name.size(), ' ');
displayed_name += " /";
}
if (is_hovered) {
glColor4f(1.0f, 1.0f, 1.0f, kMenuAlpha);
} else if (is_selected) {
glColor4f(0.2f, 0.8f, 0.2f, kMenuAlpha);
} else {
glColor4f(0.7f, 0.7f, 0.7f, kMenuAlpha);
}
PrintString(ToAbsoluteX(left + kMenuBorderWidth), ToAbsoluteY(bottom - 5),
displayed_name);
// Display submenu if any and selected.
if (is_selected && !token.next.empty()) {
std::string suffix =
current_selection->substr(/*pos*/ token.name.size() + 1);
*current_selection =
current_selection->substr(/*pos*/ 0, /*n*/ token.name.size() + 1);
was_hovered |= DisplaySubMenu(right + 1, top, token, &suffix);
*current_selection += suffix;
}
top += kMenuItemHeight;
}
if (mouse_in_submenu) assert(was_hovered);
if (mouse_is_nearby && !was_hovered) *current_selection = "";
return was_hovered || mouse_is_nearby;
}
// Renders the menu and selected or hovered submenus.
void Params::DisplayMenu() {
const bool is_hamburger_hovered =
(mouse_x_ >= 0 && mouse_x_ < kMenuItemHeight && mouse_y_ >= 0 &&
mouse_y_ < kMenuItemHeight);
// Show the hamburger menu.
if (IsHamburgerMenuVisible()) {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glColor4f(0.f, 0.f, 0.f, .8f);
glRectf(-1, 1, -1 + ToRelativeX(kMenuItemHeight),
1 - ToRelativeY(kMenuItemHeight));
glColor4f(1.f, 1.f, 1.f, .8f);
for (uint32_t i = 0; i < 3; ++i) {
glRectf(-1 + ToRelativeX(2), 1 - ToRelativeY(i * 5 + 3),
-1 + ToRelativeX(kMenuItemHeight - 2),
1 - ToRelativeY(i * 5 + 5));
}
// Show menu items if hamburger menu is hovered.
if (is_hamburger_hovered) show_ = kMenu;
} else if (show_ == kMenu) {
show_ = kDebug;
menu_selection_ = visual_debug_;
}
// Show menu items.
if (show_ == kMenu) {
const bool menu_hovered =
DisplaySubMenu(0, kMenuItemHeight, kVisualDebug, &menu_selection_);
if (!menu_hovered && !is_hamburger_hovered) {
show_ = kDebug;
menu_selection_ = visual_debug_;
}
}
}
//------------------------------------------------------------------------------
// Messages
constexpr uint32_t kFontWidthPix = 9;
// Split 'str' by the line break delimiter into 'msg' entries.
void SplitIntoMessages(const std::string& str,
std::vector<std::string>* const msg) {
std::string::size_type from = 0, to;
while (from < str.size()) {
if ((to = str.find('\n', from)) == std::string::npos) to = str.size();
msg->emplace_back(str.substr(from, to - from));
from = to + 1;
}
// Remove any empty line at the back.
while (!msg->empty() && msg->back().empty()) msg->pop_back();
}
void Params::PrintMessages(const std::vector<std::string>& msg,
bool print_lower, const float text_color[4],
const float bg_color[4], bool small, bool outline) {
if (msg.empty()) return;
const float line_height = ToRelativeY(19);
const float line_start =
1.f - ToRelativeY(print_lower ? 25 : 5) - line_height;
const float left_start = -1.f + ToRelativeX(10);
// Background unless fully transparent
if (bg_color[3] > 0) {
const float y_top = line_start + line_height;
const float x_left = left_start - ToRelativeX(5);
float HY = msg.size() * line_height;
float HX = 0;
for (const std::string& str : msg) HX = std::max(HX, (float)str.size());
HX *= ToRelativeX(kFontWidthPix);
HX += ToRelativeX(10);
HY += ToRelativeY(10);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glColor4f(bg_color[0], bg_color[1], bg_color[2], bg_color[3]);
glRectf(x_left, y_top, x_left + HX, y_top - HY);
}
if (outline) {
glColor4f(bg_color[0], bg_color[1], bg_color[2], text_color[3]);
for (int dy = -1; dy <= 1; dy += 2) {
for (int dx = -1; dx <= 1; dx += 2) {
for (size_t i = 0; i < msg.size(); ++i) {
const float position = line_start - i * line_height;
PrintString(left_start + ToRelativeX(dx), position + ToRelativeY(dy),
msg[i], small);
}
}
}
}
// Text
glColor4f(text_color[0], text_color[1], text_color[2], text_color[3]);
for (size_t i = 0; i < msg.size(); ++i) {
const float position = line_start - i * line_height;
PrintString(left_start, position, msg[i], small);
}
}
#if defined(WP2_BITTRACE)
constexpr uint32_t kBitTraceMaxLevels = 10;
void Params::DisplayBitTraces() const {
double total_size = 0;
for (const auto& p : dinfo_.bit_traces) total_size += p.second.bits;
std::map<std::string, float> traces;
for (const auto& p : dinfo_.bit_traces) {
uint32_t level = 0;
size_t i = 0;
while (level < visual_bit_trace_level_ && i != std::string::npos) {
i = p.first.find("/", (i == 0) ? 0 : i + 1);
traces[p.first.substr(0, i)] += p.second.bits;
++level;
}
}
const float line_height = ToRelativeY(35);
const float mouse_x = ToRelativeX(mouse_x_) - 1;
const float mouse_y = -(ToRelativeY(mouse_y_) - 1);
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
// First pass draws the bars, second pass draws the labels, third pass draws
// hovered label.
for (uint32_t pass = 0; pass < 3; ++pass) {
std::map<std::string, float> left_pos;
uint32_t index[kBitTraceMaxLevels] = {0};
for (const auto& v : traces) {
const float relative_size = v.second / total_size;
const float width = relative_size * 2;
const int level = std::count(v.first.begin(), v.first.end(), '/');
std::string prefix = "";
if (level > 0) {
prefix = v.first.substr(0, v.first.find_last_of("/"));
}
const float bottom = -1 + line_height * level;
const float top = bottom + line_height;
const float left = left_pos[prefix] - 1;
const float right = left + width;
constexpr float kInfoAlpha = 0.8f;
if (pass == 0) {
// First pass : draw the bars.
const GLfloat color = index[level] % 2;
glColor4f(color, color, color, kInfoAlpha);
glRectf(left, top, right, bottom);
// Add a black line at the top.
glColor4f(0.f, 0.f, 0.f, kInfoAlpha);
glRectf(left, top, right, top - ToRelativeY(1));
} else if (pass == 1) {
// Second pass: draw the labels.
glColor4f(1.f, 0.f, 0.f, kInfoAlpha);
const float top_pos = bottom + ToRelativeY(10);
std::string title = v.first;
if (level > 0) {
title = title.substr(title.find_last_of("/") + 1);
}
const uint32_t num_chars = std::max(
0.f,
std::floor((width - ToRelativeX(4)) / ToRelativeX(kFontWidthPix)));
if (num_chars > 1) {
if (num_chars < title.length()) {
title = title.substr(0, num_chars - 1);
title += "."; // To show that it's truncated.
}
PrintString(left_pos[prefix] - 1 + ToRelativeX(2), top_pos, title);
}
} else if (pass == 2 && mouse_x > left && mouse_x < right &&
mouse_y < top && mouse_y > bottom) {
// Third pass: draw hover label.
std::string title = v.first;
if (level > 0) {
title = title.substr(title.find_last_of("/") + 1);
}
title += SPrintf(" (%.f B, %.2f%%)", title.c_str(), v.second / 8.f,
relative_size * 100);
const float title_width = title.size() * ToRelativeX(kFontWidthPix);
const float title_x =
(mouse_x + title_width > 1) ? 1 - title_width : mouse_x;
// Draw a black outline.
glColor4f(0.f, 0.f, 0.f, 1.f);
static constexpr int kBorderWidth = 2;
for (int dy = -kBorderWidth; dy <= kBorderWidth; ++dy) {
for (int dx = -kBorderWidth; dx <= kBorderWidth; ++dx) {
PrintString(title_x + ToRelativeX(dx), mouse_y + ToRelativeY(dy),
title);
}
}
glColor4f(1.f, 1.f, 0.f, 1.f);
PrintString(title_x, mouse_y, title);
}
++index[level];
left_pos[v.first] = left_pos[prefix];
left_pos[prefix] += width;
}
}
}
#endif
//------------------------------------------------------------------------------
std::vector<std::string> Params::GetHelp() {
std::vector<std::string> m;
// Adding a new shortcut? These are still available: j, p
m.emplace_back("Keyboard shortcuts:");
m.emplace_back(
" 'i' ............... overlay file information ('I' for YUV disto)");
m.emplace_back(" 'v'/'V' ........... cycle through visual debugging");
m.emplace_back(
" 'y'/'Y' ........... cycle through channels (if on channel view)");
m.emplace_back(" 'e'/'E' ........... shortcut for error map");
m.emplace_back(
" space ............. show the original uncompressed picture");
m.emplace_back(" tab ............... show the compressed picture");
m.emplace_back(" alt+1 / alt+2 ..... show alternate picture(s)");
m.emplace_back(
" 'a' ............... saves current canvas as alternate picture");
m.emplace_back(std::string(" 'A' ............... saves current canvas to ") +
dump_png_path_);
m.emplace_back(std::string(" alt+'a' ........... loads ") + dump_png_path_ +
std::string(" as alternate picture"));
m.emplace_back(
" up/down ........... change the compression factor by +/- 1 units");
m.emplace_back(
" left/right ........ change the compression factor by +/- 10 units");
m.emplace_back(" ctrl + arrows ..... change alpha compression factor");
m.emplace_back(" 's' ............... change encoding effort parameter");
m.emplace_back(" 't' ............... toggle segment id mode");
m.emplace_back(
" '1' ............... cycle colorspace type "
"(YCoCg,YCbCr,Custom,YIQ)");
m.emplace_back(" '2' ............... cycle UV-Mode (Adapt,420,Sharp,444)");
m.emplace_back(" '3' ............... change tile size");
m.emplace_back(" '4' ............... change the partition method");
m.emplace_back(" '5' ............... change the partition set");
m.emplace_back(" '6' ............... toggle block snapping");
m.emplace_back(" '7' ............... increase number of segments");
m.emplace_back(" '8' ............... increase SNS value");
m.emplace_back(" '9' ............... increase error diffusion strength");
m.emplace_back(" '0' ............... toggle perceptual tuning");
m.emplace_back(" 'm' ............... toggle use of random matrix");
#if !defined(_WIN32)
m.emplace_back(" 'x' ............... toggle the env variable WP2DBG");
#endif
m.emplace_back(" 'f'/'F'/alt+'f/F' . toggle filters");
m.emplace_back(" 'g'/'G' ........... decrease/reset grain amplitude");
m.emplace_back(" 'b' ............... display block side-info");
m.emplace_back(" 'k' ............... toggle metadata");
m.emplace_back(" 'o' ............... change background color (for alpha)");
m.emplace_back(" '\\' ............... show the preview if available");
m.emplace_back(" '|' ............... show the preview color if available");
m.emplace_back(" 'z'/'Z' ........... zoom in/out (resize window)");
m.emplace_back(" 'n'/'N' ........... change distortion metric");
#if defined(WP2_HAVE_WEBP)
m.emplace_back(" 'w'/'W' ........... show WebP equivalent in size / disto");
#endif
#if defined(WP2_HAVE_AOM)
m.emplace_back(" 'r' ............... show AV1 equivalent in quality factor");
m.emplace_back(" alt+'r' ........... show AV1 with same file size (slow)");
#if defined(WP2_HAVE_AOM_DBG)
m.emplace_back(" 'R' ............... copy AV1 partition (luma transforms)");
#endif
m.emplace_back(" 'u' ............... swap between lossy AV1/WP2");
#endif // defined(WP2_HAVE_AOM)
m.emplace_back(" '`' ............... jump to previously selected menu");
m.emplace_back(" '+'/'_' ........... go to next/previous file");
m.emplace_back(std::string(" 'l'/'L' ........... "
"load or dump the current bitstream as ") +
dump_wp2_path_);
m.emplace_back(" 'h' ............... show this help message");
m.emplace_back(" 'H' ............... toggle interface display");
m.emplace_back(" 'q' / 'Q' / ESC ... quit");
m.emplace_back("");
m.emplace_back("Block layout edition:");
m.emplace_back(" Editor is shown by bringing the \"blocks/partitioning\"");
m.emplace_back(" menu ('v' key) or the \"display block\" mode ('b' key).");
m.emplace_back(" Force blocks position and size by drawing rectangles with");
m.emplace_back(" the right mouse button. Remove one with right or middle ");
m.emplace_back(" click. Press 'c' to convert rectangles (dashed outline)");
m.emplace_back(" into actual forced encoded blocks (magenta outline).");
m.emplace_back(" Press 'd' to dump or 'D' to load the blocks in file at");
m.emplace_back(std::string(" ") + partition_file_path_);
m.emplace_back("Segment edition: In segment-ids view, right click to cycle");
m.emplace_back(" through segments, then press 'c' to apply. ");
m.emplace_back(" Middle click to remove.");
m.emplace_back("Predictor edition: In prediction view, right click to cycle");
m.emplace_back(" through predictors, then press 'c' to apply. Middle click");
m.emplace_back(" to remove. A red square means predictor was ignored ");
m.emplace_back(" because context is constant.");
m.emplace_back("Transform edition: In transform view, right click to cycle");
m.emplace_back(" through transform pairs, then press 'c' to apply.");
m.emplace_back(" Middle click to remove.");
return m;
}
//------------------------------------------------------------------------------
// Info
const char* const kCSPString[] = {"YCoCg", "YCbCr", "Custom", "YIQ"};
const char* const kUVModeString[] = {"UVAdapt", "UV420", "UV444", "UVAuto"};
const char* const kTileShapeString[] = {"128", "256", "512", "Wide", "Auto"};
const char* const kOnOff[] = {"Off", "On"};
const char* const kSegmentModes[] = {"Auto", "Explicit", "Implicit"};
STATIC_ASSERT_ARRAY_SIZE(kCSPString, kNumCspTypes);
STATIC_ASSERT_ARRAY_SIZE(kUVModeString, EncoderConfig::NumUVMode);
void Params::PrintInfo() {
DisplaySparks();
std::vector<std::string> msg;
DisplayBlockInfo(&msg);
DisplayPartition();
DisplayForcedSegments();
DisplayForcedPredictors();
DisplayForcedTransforms();
DisplayMenu();
if (show_ == kMenu ||
(forcing_block_.width > 0 && forcing_block_.height > 0)) {
// Do not print messages if something else is displayed instead.
return;
}
bool print_lower = IsHamburgerMenuVisible();
float text_color[4] = {.0f, .0f, .0f, .9f};
float bg_color[4] = {.8f, .8f, .9f, .9f};
bool small = false;
if (display_block_ != kDisplayNone) {
bg_color[0] = .8f;
bg_color[1] = .7f;
bg_color[2] = .7f;
bg_color[3] = .8f;
small = (display_block_ > kDisplayHeader);
} else if (show_ == kHelp) {
const std::vector<std::string> help = GetHelp();
msg.insert(msg.end(), help.begin(), help.end());
} else if (show_ == kOriginal) {
msg.emplace_back("- O R I G I N A L -");
if (!original_selection_info_.empty()) {
SplitIntoMessages(original_selection_info_, &msg);
print_lower = true; // Match standard kDebug text formatting.
small = true;
}
} else if (show_ == kCompressed) {
msg.emplace_back("- Compressed -");
} else if (show_ == kPreview) {
msg.emplace_back("- Preview -");
} else if (show_ == kPreviewColor) {
msg.emplace_back("- Preview color -");
} else if (show_ == kWebP) {
msg.emplace_back(SPrintf("- WebP [size=%u %s=%.2f] -", webp_size_,
kMetricNames[distortion_metric_],
webp_distortion_));
msg.emplace_back(SPrintf("[ref WP2: size=%u %s=%.2f]", output_.size(),
kMetricNames[distortion_metric_], distortion_[4]));
} else if (show_ == kInfo) {
bg_color[0] = .8f;
bg_color[1] = .9f;
bg_color[2] = .9f;
bg_color[3] = .8f;
small = true;
msg.emplace_back(files_[current_file_]);
msg.emplace_back(SPrintf("Dimension: %d x %d", width_, height_));
if (viewport_width_ != width_ || viewport_height_ != height_) {
msg.emplace_back(SPrintf(" (displayed as %d x %d)",
viewport_width_, viewport_height_));
}
if (!view_only_) {
msg.emplace_back(SPrintf(
"Quality: %3.1f [effort=%d %s sns=%5.1f diffusion=%d", quality_,
enc_config_.effort, enc_config_.use_av1 ? "(AV1)" : " ",
enc_config_.sns, enc_config_.error_diffusion));
msg.emplace_back(
SPrintf(" segments=%u/%d(%c) snap=%s]",
dinfo_.num_segments, enc_config_.segments,
kSegmentModes[(int)enc_config_.segment_id_mode][0],
kOnOff[enc_config_.partition_snapping]));
#if defined(WP2_BITTRACE)
msg.emplace_back(SPrintf(" %lu blocks", einfo_.blocks.size()));
#endif
if (in_.HasTransparency()) {
msg.emplace_back(SPrintf("Alpha quality: %.1f (%s=%.2f)",
alpha_quality_, kMetricNames[distortion_metric_],
distortion_[0]));
}
} else {
msg.emplace_back(" -- WARNING: Showing original WP2 image --");
}
msg.emplace_back(SPrintf("Size: %u [%.2f bpp] (", (uint32_t)output_.size(),
8.f * output_.size() / (width_ * height_)));
if (!view_only_) {
msg.back() += SPrintf("enc %u ms %u threads + ", enc_duration_ms_,
enc_config_.thread_level);
}
msg.back() += SPrintf("dec %u ms %u threads)", dec_duration_ms_,
dec_config_.thread_level);
if (!view_only_) {
msg.back() += SPrintf(" (%.1f%% of original)",
100.f * output_.size() / input_.size());
}
if (in_.metadata_.IsEmpty()) {
assert(out_.metadata_.IsEmpty());
} else {
const Metadata& mi = in_.metadata_;
const Metadata& mo = out_.metadata_;
msg.emplace_back(SPrintf( // 6 spaces to align with "Size: ".
" %s %u B of metadata (ICCP %u, XMP %u, EXIF %u)",
keep_metadata_ ? "included" : "ignored",
(uint32_t)(mi.iccp.size + mi.xmp.size + mi.exif.size),
(uint32_t)mi.iccp.size, (uint32_t)mi.xmp.size,
(uint32_t)mi.exif.size));
if (!keep_metadata_) {
assert(out_.metadata_.IsEmpty());
} else if (mi.iccp.size != mo.iccp.size || mi.xmp.size != mo.xmp.size ||
mi.exif.size != mo.exif.size) {
msg.emplace_back(SPrintf(
" decoded as ICCP %u, XMP %u, EXIF %u",
(uint32_t)mo.iccp.size, (uint32_t)mo.xmp.size,
(uint32_t)mo.exif.size));
}
}
#if defined(WP2_ENC_DEC_MATCH)
msg.emplace_back(SPrintf( // 6 spaces to align with "Size: ".
" Warning: size is inflated by WP2_ENC_DEC_MATCH"));
#endif
msg.emplace_back(SPrintf("Filters: dblk=%s, drct=%s, rstr=%s, alpha=%s",
kOnOff[dec_config_.enable_deblocking_filter],
kOnOff[dec_config_.enable_directional_filter],
kOnOff[dec_config_.enable_restoration_filter],
kOnOff[dec_config_.enable_alpha_filter]));
if (!view_only_ || !original_file_.empty()) {
msg.emplace_back(SPrintf("%s=%.2f (a %.1f, r %.1f, g %.1f, b %.1f)",
kMetricNames[distortion_metric_], distortion_[4], distortion_[0],
distortion_[1], distortion_[2], distortion_[3]));
if (yuv_distortion_[0] > 0.f) { // Maybe it is unavailable.
msg.emplace_back(SPrintf(" (y %.1f, u %.1f, v %.1f)",
yuv_distortion_[0], yuv_distortion_[1], yuv_distortion_[2]));
}
msg.emplace_back(SPrintf("CSP: %s UV: %s Partition: %s, %s Tile: %s",
kCSPString[(int)enc_config_.csp_type],
kUVModeString[(int)enc_config_.uv_mode],
kPartitionMethodString[enc_config_.partition_method],
kPartitionSetString[enc_config_.partition_set],
kTileShapeString[enc_config_.tile_shape]));
msg.emplace_back(SPrintf("Perceptual:%s RndMtx: %s Grain: %s Amp: %d",
kOnOff[enc_config_.tune_perceptual],
kOnOff[enc_config_.use_random_matrix],
kOnOff[enc_config_.store_grain], dec_config_.grain_amplitude));
}
#if defined(WP2_BITTRACE)
DisplayBitTraces();
#endif // WP2_BITTRACE
} else if (show_ == kAlt1) {
msg.emplace_back(alt1_msg_);
} else if (show_ == kAlt2) {
msg.emplace_back(alt2_msg_);
} else {
bg_color[0] = .9f;
bg_color[1] = .9f;
bg_color[2] = .9f;
bg_color[3] = .8f;
small = true;
if (VDMatch(enc_config_, "")) {
msg.emplace_back(einfo_.visual_debug);
SplitIntoMessages(einfo_.selection_info, &msg);
} else if (VDMatch(dec_config_, "")) {
msg.emplace_back(dinfo_.visual_debug);
SplitIntoMessages(dinfo_.selection_info, &msg);
}
const size_t hash = std::hash<std::string>{}(einfo_.selection_info);
constexpr uint32_t kMaxMsgLines = 35;
if (msg.size() > kMaxMsgLines) {
if (hash != last_msg_hash_) { // Only print once, not on every redisplay.
printf("=========> continued\n");
for (uint32_t i = kMaxMsgLines; i < msg.size(); ++i) {
printf("%s\n", msg[i].c_str());
}
}
msg.resize(kMaxMsgLines + 1);
msg[kMaxMsgLines] = "(abridged, see console)";
}
last_msg_hash_ = hash;
}
if (!message_.empty()) {
msg.push_back(message_);
if (glutGet(GLUT_ELAPSED_TIME) > message_end_time_) {
message_.clear();
}
}
PrintMessages(msg, print_lower, text_color, bg_color, small);
}
void Params::SetMessage(const std::string& message, bool even_if_show_info,
int duration_ms) {
if (show_ != kInfo || even_if_show_info) {
message_ = message;
message_end_time_ = glutGet(GLUT_ELAPSED_TIME) + duration_ms;
}
}
void Params::AddInfo(const std::string& message) {
SetMessage(message, /*even_if_show_info=*/true, /*duration_ms=*/5000);
}
void Params::SetError(const std::string& message) {
SetMessage(message, /*even_if_show_info=*/true,
/*duration_ms=*/60000);
// Reset outputs so that nothing misleading is displayed.
for (ArgbBuffer* const buffer : {&out_, &dinfo_.debug_output}) {
WP2_ASSERT_STATUS(buffer->Resize(in_.width(), in_.height()));
buffer->Fill({0, 0, 0, 0});
}
#if defined(WP2_BITTRACE)
einfo_.blocks.clear();
dinfo_.bit_traces.clear();
dinfo_.blocks.clear();
#endif // WP2_BITTRACE
dinfo_.header_size = 0;
dinfo_.selection_info.clear();
}
//------------------------------------------------------------------------------
// Timer callbacks
void DisplayLoop(int delay_ms) {
glutTimerFunc((unsigned int)delay_ms, DisplayLoop, delay_ms);
glutPostRedisplay();
}
//------------------------------------------------------------------------------
// error maps
WP2Status ComputeErrorMap(MetricType metric, const ArgbBuffer& original,
const ArgbBuffer& decoded,
ArgbBuffer* const error_map,
DecoderInfo* const info = nullptr) {
WP2_CHECK_OK(decoded.width() == original.width() &&
decoded.height() == original.height(), WP2_STATUS_BAD_DIMENSION);
WP2_CHECK_STATUS(error_map->Resize(original.width(), original.height()));
const double norm = (metric == PSNR) ? 5. : (metric == LSIM) ? 5. : 8.;
for (uint32_t y = 0; y < error_map->height(); ++y) {
uint8_t* const dst = error_map->GetRow8(y);
for (uint32_t x = 0; x < error_map->width(); ++x) {
float d;
WP2_CHECK_STATUS(decoded.GetDistortion(original, x, y, metric, &d));
const uint8_t v = (uint8_t)std::lround(Clamp(255. - d * norm, 0., 255.));
dst[4 * x + 0] = 255; // TODO(skal): handle alpha properly
dst[4 * x + 1] = dst[4 * x + 2] = dst[4 * x + 3] = v;
if (info != nullptr && info->selection.x == x && info->selection.y == y) {
info->selection_info =
SPrintf("%s at %u, %u: %f\n", kMetricNames[metric], x, y, d);
}
}
}
return WP2_STATUS_OK;
}
// Sets 'diff' to be a shadowed version of 'original' but with a greener or
// redder tint depending on 'error_a' or 'error_b' being the lowest.
void ComputeDiffPixel(const uint8_t original[4], uint8_t error_a,
uint8_t error_b, uint8_t diff[4]) {
diff[0] = kAlphaMax;
for (uint32_t i : {1, 2, 3}) {
// Darkened/greyed out version of the original sample.
diff[i] = RightShiftRound((63u << 1) + ((uint32_t)original[i] << 6), 7);
}
if (error_a > error_b) {
diff[1] = (uint8_t)Clamp(diff[1] + (error_a - error_b) * 10u, 0u, 255u);
} else if (error_b > error_a) {
diff[2] = (uint8_t)Clamp(diff[2] + (error_b - error_a) * 10u, 0u, 255u);
}
}
// Fills 'diff_map' with 'original' pixels that are greener if 'decoded' is
// closer than 'alt', redder otherwise.
WP2Status ComputeDiffMap(MetricType metric, const ArgbBuffer& original,
const ArgbBuffer& decoded, const ArgbBuffer& alt,
ArgbBuffer* const diff_map,
DecoderInfo* const info = nullptr) {
if (info != nullptr) {
float disto_dec[5] = {}, disto_alt[5] = {};
WP2_CHECK_STATUS(decoded.GetDistortion(original, metric, disto_dec));
WP2_CHECK_STATUS(alt.GetDistortion(original, metric, disto_alt));
info->selection_info =
SPrintf("%s of decoded: %5.2f (A %4.1f, R %4.1f, G %4.1f, B %4.1f)\n",
kMetricNames[metric], disto_dec[4], disto_dec[0], disto_dec[1],
disto_dec[2], disto_dec[3]);
info->selection_info +=
SPrintf("%s of alt: %5.2f (A %4.1f, R %4.1f, G %4.1f, B %4.1f)\n",
kMetricNames[metric], disto_alt[4], disto_alt[0], disto_alt[1],
disto_alt[2], disto_alt[3]);
}
ArgbBuffer error_map_decoded, error_map_alt;
WP2_CHECK_STATUS(
ComputeErrorMap(metric, original, decoded, &error_map_decoded));
WP2_CHECK_STATUS(ComputeErrorMap(metric, original, alt, &error_map_alt));
WP2_CHECK_STATUS(diff_map->Resize(original.width(), original.height()));
const uint32_t bpp = WP2FormatBpp(original.format());
for (uint32_t y = 0; y < diff_map->height(); ++y) {
for (uint32_t x = 0; x < diff_map->width(); ++x) {
const uint32_t i = x * bpp + 1;
const uint8_t err_dec = (error_map_decoded.GetRow8(y))[i];
const uint8_t err_alt = (error_map_alt.GetRow8(y))[i];
ComputeDiffPixel(&(original.GetRow8(y))[x * bpp], err_dec, err_alt,
&(diff_map->GetRow8(y))[x * bpp]);
if (info != nullptr && info->selection.x == x && info->selection.y == y) {
info->selection_info +=
SPrintf("Normalized diff at %3u,%3u: decoded %u alt %u\n", x, y,
err_dec, err_alt);
}
}
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
// compression
bool Params::EncodeImage() {
enc_config_.quality = quality_;
enc_config_.alpha_quality = alpha_quality_;
if (!enc_config_.IsValid()) {
SetError("Error: Invalid configuration");
fprintf(stderr, "Invalid configuration\n");
return false;
}
const double start = GetStopwatchTime();
if (view_only_) {
output_ = input_;
} else {
output_.clear();
einfo_.force_partition = force_partition_;
einfo_.force_param = force_param_;
StringWriter writer(&output_);
Metadata metadata;
using std::swap;
if (!keep_metadata_) swap(metadata, in_.metadata_);
const WP2Status enc_status = Encode(in_, &writer, enc_config_);
if (!keep_metadata_) swap(metadata, in_.metadata_);
if (enc_status != WP2_STATUS_OK) {
SetError("Error: Compression failed");
fprintf(stderr, "Compression failed: %s\n", WP2GetStatusText(enc_status));
return false;
}
}
enc_duration_ms_ =
(uint32_t)std::lround(1000. * (GetStopwatchTime() - start));
return true;
}
bool Params::EncodeAndDecode() {
if (!EncodeImage()) return false;
return DecodeOutput();
}
namespace {
void ClearForcedSegments(std::vector<EncoderInfo::ForcedParam>* params) {
std::remove_if(params->begin(), params->end(),
[](const EncoderInfo::ForcedParam& forced) {
return forced.type ==
EncoderInfo::ForcedParam::Type::kSegment;
});
}
} // namespace
bool Params::DecodeOutput() {
const double start = GetStopwatchTime();
if (view_only_ && GuessImageFormat((const uint8_t*)output_.data(),
output_.size()) != FileFormat::WP2) {
status_ = Decode(output_, &out_, dec_config_);
} else {
if (VDMatch(dec_config_, "original")) {
// The reference image is needed in 'debug_output' if the 'visual_debug'
// is set to "original".
status_ = dinfo_.debug_output.ConvertFrom(in_);
if (status_ != WP2_STATUS_OK) {
SetError("Error: Out of memory");
return false;
}
}
status_ = Decode(output_, &out_, dec_config_);
if (bit_trace_ > 0) {
#if defined(WP2_BITTRACE)
PrintBitTraces(dinfo_, output_.size(), /*sort_values=*/true,
/*use_bytes=*/bit_trace_ == 2, /*show_histograms=*/false,
/*short_version=*/false, bit_trace_level_);
#endif
}
}
dec_duration_ms_ =
(uint32_t)std::lround(1000. * (GetStopwatchTime() - start));
if (status_ != WP2_STATUS_OK) {
SetError("Error: Decompression failed");
fprintf(stderr, "Decompression failed\n");
return false;
}
if (out_.GetDistortionBlackOrWhiteBackground(in_, distortion_metric_,
distortion_) != WP2_STATUS_OK) {
fprintf(stderr, "Error while computing the distortion.\n");
}
if (VDMatch(dec_config_, "error-map")) {
const MetricType metric = VDMatch(dec_config_, "PSNR") ? PSNR
: VDMatch(dec_config_, "LSIM") ? LSIM
: VDMatch(dec_config_, "MSSSIM") ? MSSSIM
: SSIM;
ArgbBuffer in_premultiplied(WP2_Argb_32);
if (in_premultiplied.ConvertFrom(in_) != WP2_STATUS_OK) {
SetError("Error: Conversion to Argb failed!");
fprintf(stderr, "Conversion to Argb failed!\n");
return false;
}
if (VDMatch(dec_config_, "diff-with-alt")) {
if (alt1_.IsEmpty()) {
SetError("alt-1 is empty");
} else if (ComputeDiffMap(metric, in_premultiplied, out_, alt1_,
&dec_config_.info->debug_output,
dec_config_.info) != WP2_STATUS_OK) {
SetError("Error: Diff map computation failed");
fprintf(stderr, "Diff map computation failed\n");
return false;
}
} else if (ComputeErrorMap(metric, in_premultiplied, out_,
&dec_config_.info->debug_output,
dec_config_.info) != WP2_STATUS_OK) {
SetError("Error: Error map computation failed");
fprintf(stderr, "Error map computation failed\n");
return false;
}
}
if (!force_param_.empty() && !dinfo_.explicit_segment_ids) {
ClearForcedSegments(&force_param_);
}
if (!ComputeYUVDistortion()) return false;
return true;
}
bool Params::ComputeYUVDistortion() {
if (!view_only_ && show_ == kInfo) { // Only displayed during 'kInfo'.
const char* const kOrigVD[] = {"original/y", "original/u", "original/v"};
const char* const kDecVD[] = {"y/compressed", "u/compressed",
"v/compressed"};
ArgbBuffer original_yuv, decompressed_yuv;
for (Channel c : {kYChannel, kUChannel, kVChannel}) {
DecoderInfo decoder_info;
dec_config_.info = &decoder_info;
decoder_info.visual_debug = kOrigVD[c];
if (!DecodeAndSwap(&original_yuv)) return false;
decoder_info.visual_debug = kDecVD[c];
if (!DecodeAndSwap(&decompressed_yuv)) return false;
dec_config_.info = &dinfo_;
float disto[5];
WP2_ASSERT_STATUS(decompressed_yuv.GetDistortion(
original_yuv, distortion_metric_, disto));
yuv_distortion_[c] = disto[1];
}
} else {
yuv_distortion_[0] = yuv_distortion_[1] = yuv_distortion_[2] = 0.f;
}
return true;
}
bool Params::VDMatchEncDec(const char* token) const {
return VDMatch(dec_config_, token) || VDMatch(enc_config_, token);
}
bool Params::DecodeAndSwap(ArgbBuffer* const buffer) {
status_ = buffer->Swap(&dec_config_.info->debug_output); // Avoid realloc
assert(status_ == WP2_STATUS_OK);
status_ = dec_config_.info->debug_output.ConvertFrom(in_);
assert(status_ == WP2_STATUS_OK);
status_ = Decode(output_, &out_, dec_config_);
if (status_ != WP2_STATUS_OK) {
fprintf(stderr, "Decompression failed?!\n");
return false;
}
status_ = buffer->Swap(&dec_config_.info->debug_output);
assert(status_ == WP2_STATUS_OK);
return true;
}
bool Params::GetOriginal() {
if (VDMatch(dec_config_, "residuals")) {
EncoderInfo encoder_info;
Channel c = VDChannel(dec_config_);
const std::string kChannelName[] = {"y", "u", "v", "a"};
const std::string vdebug = kChannelName[c] + "/encoder/original-residuals";
encoder_info.visual_debug = vdebug.c_str();
encoder_info.selection = dinfo_.selection;
enc_config_.info = &encoder_info;
// Avoid realloc
if (in_yuv_.Swap(&enc_config_.info->debug_output) != WP2_STATUS_OK) {
return false;
}
if (!EncodeImage()) return false;
if (in_yuv_.Swap(&enc_config_.info->debug_output) != WP2_STATUS_OK) {
return false;
}
original_selection_info_ = encoder_info.selection_info;
enc_config_.info = &einfo_;
if (original_.SetView(in_yuv_) != WP2_STATUS_OK) return false;
return true;
}
DecoderInfo decoder_info;
// clang-format off
decoder_info.visual_debug = VDMatchEncDec("a") ? "original/a" :
VDMatchEncDec("y") ? "original/y" :
VDMatchEncDec("u") ? "original/u" :
VDMatchEncDec("v") ? "original/v" :
VDMatchEncDec("r") ? "original/r" :
VDMatchEncDec("g") ? "original/g" :
VDMatchEncDec("b") ? "original/b" : nullptr;
// clang-format on
decoder_info.selection =
(VDMatchEncDec("prediction/modes/long") ||
VDMatchEncDec("prediction/raw") || VDMatchEncDec("compressed"))
? dinfo_.selection
: Rectangle();
if (decoder_info.visual_debug != nullptr) {
dec_config_.info = &decoder_info;
if (!DecodeAndSwap(&in_yuv_)) return false;
original_selection_info_ = decoder_info.selection_info;
dec_config_.info = &dinfo_;
if (original_.SetView(in_yuv_) != WP2_STATUS_OK) return false;
} else {
original_selection_info_.clear();
// They use different formats so we can't use a view.
if (original_.ConvertFrom(in_) != WP2_STATUS_OK) return false;
}
return true;
}
bool Params::GetCompressed() {
DecoderInfo decoder_info;
// clang-format off
decoder_info.visual_debug = VDMatchEncDec("a") ? "a/compressed" :
VDMatchEncDec("y") ? "y/compressed" :
VDMatchEncDec("u") ? "u/compressed" :
VDMatchEncDec("v") ? "v/compressed" : nullptr;
// clang-format on
if (decoder_info.visual_debug != nullptr) {
dec_config_.info = &decoder_info;
if (!DecodeAndSwap(&out_yuv_)) return false;
dec_config_.info = &dinfo_;
} else {
out_yuv_.Deallocate();
}
return true;
}
bool Params::EncodeWebP(bool match_size) {
#if defined(WP2_HAVE_WEBP)
WebPConfig webp_config;
if (!WebPConfigPreset(&webp_config, WEBP_PRESET_DEFAULT, quality_)) {
return false;
}
if (enc_config_.thread_level > 0) ++webp_config.thread_level;
webp_config.method = DivRound(enc_config_.effort * 6, kMaxEffort);
webp_config.segments = std::min(enc_config_.segments, 4);
webp_config.sns_strength = (int)std::lround(enc_config_.sns);
webp_config.pass = 6;
if (match_size) {
webp_config.target_size = (int)output_.size();
} else {
if (distortion_metric_ != PSNR) return false;
webp_config.target_PSNR = distortion_[4];
}
if (!WebPValidateConfig(&webp_config)) return false;
MemoryWriter writer;
status_ = CompressWebP(in_, webp_config, &writer);
if (status_ != WP2_STATUS_OK) return false;
webp_size_ = writer.size_;
status_ = ReadImage(writer.mem_, writer.size_, &webp_, FileFormat::WEBP);
if (status_ != WP2_STATUS_OK) return false;
float disto[5];
status_ =
webp_.GetDistortionBlackOrWhiteBackground(in_, distortion_metric_, disto);
if (status_ != WP2_STATUS_OK) return false;
webp_distortion_ = disto[4];
show_ = kWebP;
return true;
#else
return false;
#endif // WP2_HAVE_WEBP
}
bool Params::EncodeAV1(bool copy_partition, float av1_quality,
size_t* const av1_file_size) {
#if !defined(WP2_HAVE_AOM_DBG)
if (copy_partition) return false;
#endif
#if defined(WP2_HAVE_AOM)
ParamsAV1 cfg;
cfg.quality = av1_quality;
// The settings below seem to give the best results (yes, even threads).
cfg.effort = enc_config_.effort;
cfg.threads = 1;
cfg.pass = 2;
cfg.use_yuv444 = (enc_config_.uv_mode != EncoderConfig::UVMode420);
cfg.draw_blocks = (display_block_ != kDisplayNone);
cfg.draw_transforms = (display_block_ == kDisplayYCoeffs);
if (copy_partition) force_partition_.clear();
printf("Compressing AV1 at quality %.1f (effort:%d)...\n",
cfg.quality, cfg.effort);
std::string tmp;
double timing[2];
ShiftAlt();
if (CompressAV1(
in_, cfg, &alt1_, &tmp, timing, /*blocks=*/nullptr,
/*transforms=*/(copy_partition ? &force_partition_ : nullptr)) !=
WP2_STATUS_OK) {
return false;
}
printf("Done compressing AV1 in just %.2f seconds (%u bytes)!\n", timing[0],
(uint32_t)tmp.size());
if (IoUtilWriteFile(tmp, dump_av1_path_,
/*overwrite=*/true) != WP2_STATUS_OK) {
fprintf(stderr, "Warning, could not save AV1 bitstream to %s\n",
dump_av1_path_);
} else {
printf("Saved AV1 bitstream to %s\n", dump_av1_path_);
}
if (av1_file_size != nullptr) *av1_file_size = tmp.size();
alt1_msg_ = SPrintf("AV1 - size = %u", (uint32_t)tmp.size());
if (!cfg.draw_blocks && !cfg.draw_transforms) {
// Only print distortion if the output image is unaltered by debug overlays.
float disto[5];
if (alt1_.GetDistortionBlackOrWhiteBackground(in_, distortion_metric_,
disto) != WP2_STATUS_OK) {
return false;
}
alt1_msg_ +=
SPrintf(" %s = %.1f", kMetricNames[distortion_metric_], disto[4]);
}
if (copy_partition) {
ConvertPartition(in_.width(), in_.height(), enc_config_.partition_set,
/*ignore_invalid=*/true, &force_partition_);
}
show_ = kAlt1;
return true;
#else
(void)copy_partition;
return false;
#endif // WP2_HAVE_AOM
}
bool Params::EncodeAV1ToMatch(bool copy_partition, size_t target_file_size) {
// Begin by testing 0, it's probably a good choice.
int lo_q = -100, hi_q = 100, mid_q;
do {
mid_q = (lo_q + hi_q) / 2;
size_t file_size;
if (!EncodeAV1(copy_partition, mid_q, &file_size)) {
AddInfo("AV1 not available");
return false;
}
if (std::abs((float)file_size - target_file_size) <
0.001f * target_file_size) {
break;
} else if (file_size < target_file_size) {
lo_q = mid_q + 1;
} else {
hi_q = mid_q;
}
} while (lo_q >= 0 && lo_q < hi_q);
return true;
}
//------------------------------------------------------------------------------
void Params::UpdateViewZoomLevel(int incr) {
if (view_zoom_level_ + incr <= 0) {
zoom_level_ -= view_zoom_level_;
view_zoom_level_ = 0;
} else if ((width_ >> (view_zoom_level_ + incr) > 1) &&
(height_ >> (view_zoom_level_ + incr) > 1)) {
view_zoom_level_ += incr;
// Also change the zoom level so the window size doesn't change.
zoom_level_ += incr;
}
if (view_zoom_level_ == 0) {
view_rect_ = {0, 0, width_, height_};
} else {
const int view_w = std::ceil((float)width_ / (1 << view_zoom_level_));
const int view_h = std::ceil((float)height_ / (1 << view_zoom_level_));
const int img_x_down = ToImageX(mouse_x_down_);
const int img_y_down = ToImageY(mouse_y_down_);
// Compute view x/y so that the last point that was clicked stays in the
// same place.
const int view_x =
Clamp<int>(img_x_down - view_w * mouse_x_down_ / viewport_width_, 0,
width_ - view_w);
const int view_y =
Clamp<int>(img_y_down - view_h * mouse_y_down_ / viewport_height_, 0,
height_ - view_h);
view_rect_ = {(uint32_t)view_x, (uint32_t)view_y, (uint32_t)view_w,
(uint32_t)view_h};
}
}
void Params::ApplyZoomLevel() {
if (zoom_level_ >= 0) {
viewport_width_ = view_rect_.width << zoom_level_;
viewport_height_ = view_rect_.height << zoom_level_;
} else {
viewport_width_ = view_rect_.width >> -zoom_level_;
viewport_height_ = view_rect_.height >> -zoom_level_;
}
}
bool Params::SetCurrentFile(uint32_t file_number) {
if (file_number >= files_.size()) file_number = files_.size() - 1;
if (file_number == current_file_) return WP2_STATUS_OK;
ClearForcedElements();
current_file_ = file_number;
const std::string& file_name = files_[file_number];
status_ = IoUtilReadFile(file_name.c_str(), &input_);
if (status_ != WP2_STATUS_OK) {
fprintf(stderr, "Could not read the input file %s\n", file_name.c_str());
return false;
}
const uint8_t* data = (const uint8_t*)input_.data();
const size_t data_size = input_.size();
const FileFormat format = GuessImageFormat(data, data_size);
if (format == FileFormat::UNSUPPORTED) {
fprintf(stderr, "Unsupported input format\n");
return false;
}
if (ReadImage(data, data_size, &in_, format) != WP2_STATUS_OK) {
fprintf(stderr, "Could not decode the input file %s\n", file_name.c_str());
return false;
}
if (view_only_ && !original_file_.empty()) {
ArgbBuffer tmp_orig;
if (ReadImage(original_file_.c_str(), &tmp_orig) != WP2_STATUS_OK) {
fprintf(stderr, "Warning: could not decode the original file %s\n",
original_file_.c_str());
} else if (tmp_orig.width() != in_.width() ||
tmp_orig.height() != in_.height()) {
fprintf(stderr,
"Warning: original file dimensions %d x %d don't match encoded "
"file %d x %d\n",
tmp_orig.width(), tmp_orig.height(), in_.width(), in_.height());
} else if (in_.Swap(&tmp_orig) != WP2_STATUS_OK) {
return false;
}
}
if (crop_ && in_.SetView(in_, crop_area_) != WP2_STATUS_OK) {
fprintf(stderr, "Error! Cropping operation failed. Skipping.");
}
preview_color_ = {0x00u, 0x00u, 0x00u, 0x00u};
preview_.Deallocate();
if (format == FileFormat::WP2) {
BitstreamFeatures wp2_features;
if (wp2_features.Read(data, data_size) != WP2_STATUS_OK) {
fprintf(stderr, "Inconsistent state: could not read WP2 features.\n");
return false;
}
if (wp2_features.has_preview &&
ExtractPreview(data, data_size, &preview_) != WP2_STATUS_OK) {
fprintf(stderr, "Could not decode preview of %s\n", file_name.c_str());
preview_.Deallocate();
}
preview_color_ = ToArgb32b(wp2_features.preview_color);
}
width_ = in_.width();
height_ = in_.height();
view_zoom_level_ = 0;
view_rect_ = {0, 0, width_, height_};
if (!EncodeAndDecode()) return false;
return (status_ == WP2_STATUS_OK);
}
void Params::ShiftAlt() {
const WP2Status s = alt1_.Swap(&alt2_);
(void)s;
assert(s == WP2_STATUS_OK);
std::swap(alt1_msg_, alt2_msg_);
alt1_msg_.clear();
alt1_.Deallocate();
}
bool Params::SetBitstream(const char* const file_name) {
std::string data;
status_ = IoUtilReadFile(file_name, &data);
if (status_ != WP2_STATUS_OK) {
fprintf(stderr, "Could not read the file %s\n", file_name);
return false;
}
ArgbBuffer tmp;
status_ = Decode(data, &tmp);
if (status_ != WP2_STATUS_OK) {
fprintf(stderr, "Could not decode the file %s\n", file_name);
return false;
}
ClearForcedElements();
alt1_msg_.clear();
alt1_.Deallocate();
alt2_msg_.clear();
alt2_.Deallocate();
view_only_ = true;
using std::swap;
swap(data, input_);
data.clear();
status_ = tmp.Swap(&in_);
return (status_ == WP2_STATUS_OK);
}
bool Params::SetAltFile(const char* const file_name) {
ShiftAlt();
status_ = ReadImage(file_name, &alt1_);
if (status_ != WP2_STATUS_OK) {
fprintf(stderr, "Could not decode the alternate file %s\n", file_name);
return false;
}
const uint32_t w = alt1_.width(), h = alt1_.height();
if (w != width_ || h != height_) {
alt1_.Deallocate();
fprintf(stderr,
"Alternate picture has incompatible dimensions "
" (%dx%d vs expected %dx%d)\n",
w, h, width_, height_);
return false;
}
alt1_msg_ = SPrintf("- Alt Pic (%s) -", file_name);
return true;
}
// Copies the current canvas to the alternative buffer.
bool Params::SetAltImage() {
if (show_ == kAlt1 || show_ == kAlt2) return false;
ArgbBuffer tmp;
if (GetCurrentCanvas(&tmp)) {
ShiftAlt();
status_ = tmp.IsView() ? alt1_.CopyFrom(tmp) : alt1_.Swap(&tmp);
if (status_ == WP2_STATUS_OK) alt1_msg_ = "- Saved canvas -";
}
if (status_ != WP2_STATUS_OK) {
fprintf(stderr, "Could not copy the alternate image\n");
return false;
}
return true;
}
bool Params::DumpCurrentCanvas(const char* const file_path) {
ArgbBuffer tmp;
if (GetCurrentCanvas(&tmp)) {
status_ = SaveImage(tmp, file_path, /*overwrite=*/true);
}
if (status_ != WP2_STATUS_OK) {
fprintf(stderr, "Could not dump the current canvas\n");
return false;
}
return true;
}
// Returns the currently displayed buffer.
const ArgbBuffer& Params::GetBuffer() const {
if (show_ == kOriginal) return original_;
if (show_ == kPreview) return preview_;
if (show_ == kWebP) return webp_;
if (show_ == kAlt1 && !alt1_.IsEmpty()) return alt1_;
if (show_ == kAlt2 && !alt2_.IsEmpty()) return alt2_;
if (show_ == kCompressed) return out_yuv_.IsEmpty() ? out_ : out_yuv_;
if (VDMatch(dec_config_, "")) return dec_config_.info->debug_output;
if (VDMatch(enc_config_, "") && !view_only_) {
return enc_config_.info->debug_output;
}
return out_;
}
// Copies the current canvas to 'buffer'.
bool Params::GetCurrentCanvas(ArgbBuffer* const buffer) {
if (show_ == kPreviewColor) {
ShiftAlt();
status_ = buffer->Resize(out_.width(), out_.height());
if (status_ == WP2_STATUS_OK) buffer->Fill(preview_color_);
} else if (GetBuffer().format() == buffer->format()) {
status_ = buffer->SetView(GetBuffer());
} else {
status_ = buffer->ConvertFrom(GetBuffer());
}
return (status_ == WP2_STATUS_OK);
}
//------------------------------------------------------------------------------
// Blocks
void Params::glRect(const Rectangle& rect, uint32_t thickness) const {
if (!view_rect_.Contains(rect.x, rect.y)) return;
const float x = 2.f * (rect.x - view_rect_.x) / view_rect_.width - 1.f;
// Compensate OpenGL bottom-left origin by shifting by one pixel vertically.
const float y =
1.f - 2.f * (rect.y - view_rect_.y + rect.height) / view_rect_.height -
1.f / viewport_height_;
const float w = 2.f * rect.width / view_rect_.width;
const float h = 2.f * rect.height / view_rect_.height;
// Thickness
const float px_w = 2.f / viewport_width_;
const float px_h = 2.f / viewport_height_;
for (uint32_t j = 0; j < thickness; ++j) {
for (uint32_t i = 0; i < thickness; ++i) {
if (rect.width == 0 || rect.height == 0) {
glBegin(GL_LINES);
glVertex2f(x + i * px_w, y - j * px_h);
glVertex2f(x + w + i * px_w, y + h - j * px_h);
glEnd();
} else {
glRectf(x + i * px_w, y - j * px_h, x + w + i * px_w, y + h - j * px_h);
}
}
}
}
//------------------------------------------------------------------------------
// DisplayBlockInfo()
#if defined(WP2_BITTRACE)
// Helper class for the creation of a nice looking paragraph.
class ParagraphMaker {
public:
// Initializes with a potential prefix that will define an indentation for the
// rest of the paragraph.
void Init(const std::string& str = "") {
strs_.clear();
strs_.push_back(str);
indentation_ = std::string(str.length(), ' ');
}
// Appends a string to the last string of the list.
void Append(const std::string& str) {
assert(!strs_.empty());
strs_.back() += str;
}
// Appends a new element to the list.
void AppendToList(const std::string& str) {
assert(!strs_.empty());
strs_.back() += str + ", ";
if (strs_.back().size() >= 50u) strs_.push_back(indentation_);
}
// Push back all the elements of the list to the msg.
void PushBackTo(std::vector<std::string>* const msg) {
if (strs_.empty()) return;
// Remove the last element if empty.
if (strs_.back() == indentation_) strs_.pop_back();
if (strs_.empty()) return;
// Remove the last ', '.
const uint32_t index = strs_.back().size() - 2;
if (strs_.back().substr(index) == ", ") {
strs_.back() = strs_.back().substr(0, index);
}
msg->insert(msg->end(), strs_.begin(), strs_.end());
}
private:
std::vector<std::string> strs_;
std::string indentation_;
};
constexpr uint32_t kLastChannel = 4;
constexpr const char* const kEncNames[] = {
"X", "Mtd0", "Mtd1", "DC ", "Zero", "AOM",
};
// Isolate the bit costs about residuals (Y, U, V, A, remaining stuff).
static void IsolateBitTraces(const BlockInfo& b,
std::map<const std::string, LabelStats> bit_traces[],
double bit_sums[]) {
for (const auto& bt : b.bit_traces) {
std::string label = bt.first;
uint32_t channel;
for (channel = 0; channel < kLastChannel; ++channel) {
const std::string prefix = kCoeffsStr[channel];
if (label.size() >= prefix.size() &&
std::strncmp(label.c_str(), prefix.c_str(), prefix.size()) == 0) {
// 'label' starts with 'prefix', remove 'prefix' from it.
assert(label.size() > prefix.size() + 1 &&
label[prefix.size()] == '/');
label = label.substr(prefix.size() + 1);
break;
}
}
// Merge by first token.
const size_t slash_pos = label.find('/');
if (slash_pos != std::string::npos) label = label.substr(0, slash_pos);
bit_traces[channel][label].bits += bt.second.bits;
bit_traces[channel][label].num_occurrences += bt.second.num_occurrences;
bit_sums[channel] += bt.second.bits;
}
}
void Params::PrintBlockHeader(const BlockInfo& b,
std::vector<std::string>* const msg) const {
std::map<const std::string, LabelStats> bit_traces[kLastChannel + 1];
double bit_sums[kLastChannel + 1] = {0};
IsolateBitTraces(b, bit_traces, bit_sums);
ParagraphMaker pm;
pm.Init("Costs: ");
for (uint32_t c = 0; c < (b.has_lossy_alpha ? kLastChannel : 3); ++c) {
pm.AppendToList(SPrintf("%s: %.1f", kCoeffsStr[c], bit_sums[c]));
}
for (const auto& p : bit_traces[kLastChannel]) {
pm.AppendToList(SPrintf("%s: %.1f", p.first.c_str(), p.second.bits));
}
pm.PushBackTo(msg);
msg->push_back(SPrintf("segment id: %d (%s %s)", b.segment_id,
b.is420 ? "is420" : "",
b.has_lossy_alpha ? "has_alpha_res" : ""));
msg->push_back(SPrintf("transform: %s %s",
WP2TransformNames[b.tf_x],
WP2TransformNames[b.tf_y]));
for (uint32_t tf_i = 0; tf_i < 4; ++tf_i) {
if (b.encoding_method[kYChannel][tf_i] == -1) continue;
msg->push_back(
SPrintf("Transform %u. Mthd: Y=%s U=%s V=%s (A=%s)", tf_i,
kEncNames[b.encoding_method[kYChannel][tf_i] + 1],
kEncNames[b.encoding_method[kUChannel][tf_i] + 1],
kEncNames[b.encoding_method[kVChannel][tf_i] + 1],
kEncNames[b.encoding_method[kAChannel][tf_i] + 1]));
}
}
void Params::PrintResiduals(const BlockInfo& b, const BlockInfo& b_enc,
std::vector<std::string>* const msg) const {
std::map<const std::string, LabelStats> bit_traces[kLastChannel + 1];
double bit_sums[kLastChannel + 1] = {0};
IsolateBitTraces(b, bit_traces, bit_sums);
ParagraphMaker pm;
const uint32_t channel =
(display_block_ == kDisplayYCoeffs) ? kYChannel :
(display_block_ == kDisplayUCoeffs) ? kUChannel :
(display_block_ == kDisplayVCoeffs) ? kVChannel : kAChannel;
msg->push_back(SPrintf("%s channel.", kChannelStr[channel]));
for (uint32_t tf_i = 0; tf_i < 4; ++tf_i) {
if (!b.residual_info[channel][tf_i].empty()) {
pm.Init();
// Display info about the residuals (index of last element ...).
pm.Append(SPrintf("Transform %u. Residuals: ", tf_i));
for (const std::string& str : b.residual_info[channel][tf_i]) {
pm.AppendToList(str);
}
pm.PushBackTo(msg);
}
}
// Display stats about residuals.
{
typedef decltype(b.bit_traces)::value_type StatType;
std::vector<StatType> stats(bit_traces[channel].begin(),
bit_traces[channel].end());
// Sort those stats from lowest to highest usage.
std::vector<uint32_t> indices(stats.size());
std::iota(indices.begin(), indices.end(), 0);
std::sort(indices.begin(), indices.end(),
[stats](uint32_t i1, uint32_t i2) {
return (stats[i1].second.bits < stats[i2].second.bits);
});
pm.Init("Costs: ");
pm.Append(SPrintf("all: %.1f, ", bit_sums[channel]));
// Display stats as bit cost (/num of occurences = individual bit cost).
for (uint32_t i : indices) {
const auto& p = stats[i];
pm.AppendToList(
SPrintf("%s: %.1f (/%d=%.1f)", p.first.c_str(), p.second.bits,
p.second.num_occurrences,
p.second.bits / p.second.num_occurrences));
}
pm.PushBackTo(msg);
}
const int bw = b.rect.width, bh = b.rect.height;
for (uint32_t tf_i = 0; tf_i < 4; ++tf_i) {
if (b.encoding_method[channel][tf_i] == -1) continue;
msg->push_back(SPrintf("Enc %u. Mthd: %s", tf_i,
kEncNames[b.encoding_method[channel][tf_i] + 1]));
const uint32_t scale =
(b.is420 && (channel == 1 || channel == 2)) ? 2 : 1;
const BlockSize split_size = GetSplitSize(
GetBlockSize(bw / kMinBlockSizePix, bh / kMinBlockSizePix),
b.split_tf[channel]);
const uint32_t w = BlockWidthPix(split_size) / scale;
const uint32_t h = BlockHeightPix(split_size) / scale;
const bool show_original = (show_ == kOriginal);
if (show_original || b.encoding_method[channel][tf_i] !=
(int8_t)EncodingMethod::kAllZero) {
int16_t res[kMaxBlockSizePix2];
int32_t coeffs[kMaxBlockSizePix2];
const uint32_t num_coeffs = w * h;
std::copy(&b_enc.original_res[channel][tf_i][0],
&b_enc.original_res[channel][tf_i][0] + num_coeffs,
&res[0]);
const bool reduced_transform =
(channel == kUChannel || channel == kVChannel) && b_enc.is420;
WP2Transform2D(res,
(WP2TransformType)b_enc.tf_x,
(WP2TransformType)b_enc.tf_y,
w, h, coeffs, reduced_transform);
const float norm = 2. * (int32_t)std::sqrt(num_coeffs);
for (uint32_t j = 0; j < h; ++j) {
std::string line;
for (uint32_t i = 0; i < w; ++i) {
const uint32_t idx = i + w * j;
if (show_original) {
const int32_t v = coeffs[idx];
if (std::abs(v) >= (norm * 1.00)) {
line += SPrintf("%3d ", (int)(v / norm));
} else {
const char symbol = (std::abs(v) >= (norm * 0.50)) ? 'X'
: (std::abs(v) >= (norm * 0.25)) ? 'x'
: '.';
line += SPrintf(" %c ", symbol);
}
} else {
const int32_t v = b.coeffs[channel][tf_i][idx];
line += v ? SPrintf("%3d ", v) : " . ";
}
}
msg->push_back(line);
}
if (show_original) {
msg->push_back(SPrintf("original coeffs in units of %.1f", norm));
}
} else {
msg->push_back(" - no coeffs -");
}
}
}
void Params::PrintPredModes(const BlockInfo& b,
std::vector<std::string>* const msg) const {
msg->push_back(SPrintf("Chroma prediction mode: %d", b.uv_pred));
msg->push_back(SPrintf("Luma prediction: %d", b.y_pred));
if (b.has_lossy_alpha) {
msg->push_back(SPrintf("Alpha prediction: %d", b.a_pred));
} else {
msg->push_back("-no alpha-");
}
msg->push_back(b.y_context_is_constant ? "Y context is constant"
: "Y context is not constant");
const int optimize_modes =
(b.y_context_is_constant || enc_config_.effort == 0) ? 0 :
(enc_config_.effort == 1) ? 1 : 2;
msg->push_back(
SPrintf("OptimizeModes%d() Y score: %.1f (%.1f per pixel)",
optimize_modes, b.pred_scores[kYChannel],
b.pred_scores[kYChannel] / b.rect.GetArea()));
msg->push_back(SPrintf("FindBestUVModes() score: %.1f (%.1f per pixel)",
b.pred_scores[kUChannel],
b.pred_scores[kUChannel] / b.rect.GetArea()));
}
#endif // WP2_BITTRACE
void Params::DisplayBlockInfo(std::vector<std::string>* const msg) {
if (display_block_ == kDisplayNone) return;
#if defined(WP2_BITTRACE)
assert(dinfo_.store_blocks && einfo_.store_blocks);
if (!view_only_) {
assert(einfo_.blocks.size() == dinfo_.blocks.size());
}
// Sort encoded and decoded blocks for easy comparison.
for (std::vector<BlockInfo>* blocks : {&dinfo_.blocks, &einfo_.blocks}) {
std::sort(blocks->begin(), blocks->end(),
[](const BlockInfo& l, const BlockInfo& r) {
return (l.rect.x < r.rect.x) ||
(l.rect.x == r.rect.x && l.rect.y < r.rect.y);
});
}
const int selection_x = (int)dinfo_.selection.x;
const int selection_y = (int)dinfo_.selection.y;
for (uint32_t index = 0; index < dinfo_.blocks.size(); ++index) {
const BlockInfo& b = dinfo_.blocks[index];
const BlockInfo& b_enc = view_only_ ? b : einfo_.blocks[index];
assert(b.rect == b_enc.rect);
assert(b.segment_id == b_enc.segment_id);
assert(b.y_context_is_constant == b_enc.y_context_is_constant);
const int bx = b.rect.x, by = b.rect.y;
const int bw = b.rect.width, bh = b.rect.height;
if (!view_rect_.Contains(bx, by)) continue;
const bool block_selected = b.rect.Contains(selection_x, selection_y);
if (display_block_ == kDisplayGrid || !block_selected) {
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glColor4f(0.0f, 0.2f, 0.1f, 0.2f);
glRect(b.rect);
continue;
}
bool forced = false;
for (const Rectangle& rect : einfo_.force_partition) {
if (rect == b.rect) {
forced = true;
break;
}
}
msg->push_back(SPrintf("size: %2d x %2d pos: %3d x %3d %s",
bw, bh, bx, by, forced ? "(forced)" : ""));
float disto[5];
const Rectangle rect = b.rect.ClipWith({0, 0, width_, height_});
WP2_ASSERT_STATUS(out_.GetDistortionBlackOrWhiteBackground(
in_, rect, distortion_metric_, disto));
msg->push_back(SPrintf("bits:%5.1f (%.2f bpp) %s:%5.1f", b.bits,
b.bits / b.rect.GetArea(),
kMetricNames[distortion_metric_], disto[4]));
if (display_block_ == kDisplayHeader) {
PrintBlockHeader(b, msg);
} else if (display_block_ <= kDisplayACoeffs) {
if (display_block_ == kDisplayACoeffs && !b.has_lossy_alpha) {
msg->push_back("- no alpha -");
} else {
PrintResiduals(b, b_enc, msg);
}
} else if (display_block_ == kDisplayPredModes) {
PrintPredModes(b_enc, msg);
}
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glColor4f(1.f, 0.f, 0.f, 0.5f); // red
glRect(b.rect);
}
#endif // WP2_BITTRACE
}
//------------------------------------------------------------------------------
void Params::ClearForcedElements() {
force_partition_.clear();
force_param_.clear();
forcing_block_ = Rectangle();
}
bool Params::IsDebugViewVisible() const {
return (show_ == kDebug || show_ == kInfo || show_ == kHelp ||
show_ == kMenu);
}
bool Params::IsPartitionVisible() const {
return (display_block_ != kDisplayNone ||
(IsDebugViewVisible() && VDMatch(dec_config_, "partition"))) &&
!IsSegmentVisible() && !IsPredictorVisible() && !IsTransformVisible();
}
bool Params::IsSegmentVisible() const {
return IsDebugViewVisible() && VDMatch(dec_config_, "segment-ids");
}
bool Params::IsPredictorVisible() const {
return IsDebugViewVisible() && VDMatch(dec_config_, "prediction") &&
!VDMatch(dec_config_, "lossless/prediction");
}
bool Params::IsTransformVisible() const {
return IsDebugViewVisible() && VDMatch(dec_config_, "transform");
}
void Params::DisplayPartition() const {
if (!IsPartitionVisible()) return;
// Adapt to zoom.
const uint32_t thick = getLineThickness();
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// Encoded forced blocks.
glColor4f(1.0f, 0.2f, 1.0f, 1.0f);
for (const Rectangle& rect : einfo_.force_partition) {
if (!view_rect_.Contains(rect.x, rect.y)) continue;
glRect(rect, thick);
}
glLineStipple(2 * thick, 0xAAAA);
glEnable(GL_LINE_STIPPLE);
// All blocks, including those that haven't been applied yet.
glColor4f(0.6f, 1.0f, 0.6f, 1.0f);
for (const Rectangle& rect : force_partition_) glRect(rect, thick);
const bool split_visible = VDMatch(dec_config_, "split-tf");
// If a block is currently being drawn with the mouse.
if (forcing_block_.width > 0 && forcing_block_.height > 0 &&
(moved_since_last_down_ || !split_visible)) {
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
glRect(forcing_block_, thick);
}
// Draw split_tf's.
glLineStipple(1.5 * thick, 0xAAAA);
#if defined(WP2_BITTRACE)
if (split_visible) {
for (const auto& forced : force_param_) {
if (forced.type != EncoderInfo::ForcedParam::Type::kSplitTf) continue;
if (!view_rect_.Contains(forced.x, forced.y)) continue;
const BlockInfo* const b = einfo_.FindBlock(forced.x, forced.y);
assert(b != nullptr);
if (forced.value) {
glColor4f(0.0f, 0.8f, 0.0f, 1.0f);
} else {
glColor4f(1.0f, 0.2f, 0.0f, 1.0f);
}
const uint32_t bw = b->rect.width;
const uint32_t bh = b->rect.height;
const BlockSize split_size = GetSplitSize(
GetBlockSize(bw / kMinBlockSizePix, bh / kMinBlockSizePix),
/*split=*/true);
const uint32_t split_w = BlockWidthPix(split_size);
const uint32_t split_h = BlockHeightPix(split_size);
for (uint32_t x = split_w; x < bw - 1; x += split_w) {
glRect({b->rect.x + x, b->rect.y, 0, bh}, thick);
}
for (uint32_t y = split_h; y < bh - 1; y += split_h) {
glRect({b->rect.x, b->rect.y + y, bw, 0}, thick);
}
}
}
#endif
glDisable(GL_LINE_STIPPLE);
}
void Params::DisplayForcedSegments() const {
if (!IsSegmentVisible()) return;
#if defined(WP2_BITTRACE)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
const uint32_t thick = getLineThickness();
glLineStipple(2 * thick, 0xAAAA);
for (const auto& forced : force_param_) {
if (forced.type != EncoderInfo::ForcedParam::Type::kSegment) continue;
if (!view_rect_.Contains(forced.x, forced.y)) continue;
const BlockInfo* const b = einfo_.FindBlock(forced.x, forced.y);
if (b != nullptr) {
const Rectangle rect{b->rect.x, b->rect.y,
b->rect.width - 1, b->rect.height - 1};
glColor4f(0.f, 0.f, 0.f, 1.f);
glRect(rect, thick * 2);
const Argb32b color = kSegmentColors[forced.value];
glColor4f(color.r / 255.f, color.g / 255.f, color.b / 255.f, 1.f);
glRect(rect, thick);
}
}
#endif
}
void Params::DisplayForcedPredictors() const {
if (!IsPredictorVisible()) return;
#if defined(WP2_BITTRACE)
Channel channel = VDChannel(dec_config_);
if (channel == kVChannel) channel = kUChannel;
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
for (const auto& forced : force_param_) {
if (forced.type != EncoderInfo::ForcedParam::Type::kPredictor) continue;
if (forced.channel != channel) continue;
if (!view_rect_.Contains(forced.x, forced.y)) continue;
const BlockInfo* const b = einfo_.FindBlock(forced.x, forced.y);
if (b != nullptr) {
if (channel == kYChannel && b->y_context_is_constant) {
glColor4f(1.f, 0.f, 0.f, 1.f);
} else {
glColor4f(0.f, 1.f, 0.f, 1.f);
}
glRect(b->rect, 4.f);
const std::vector<std::string>& predictors =
(channel == kYChannel) ? dinfo_.y_predictors :
(channel == kAChannel) ? dinfo_.a_predictors :
dinfo_.uv_predictors;
const uint32_t predictor_id =
std::min(forced.value, (uint32_t)(predictors.size() - 1));
const float color = (float)predictor_id / (