blob: 1c596cf48e01f2f5950a28983dfd87b6558bab49 [file] [log] [blame]
// 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.
// -----------------------------------------------------------------------------
//
// To and from Y4M conversion command line tool.
//
// Author: Yannis Guyon (yguyon@google.com)
#include <array>
#include <cstdio>
#include <cstring>
#include <string>
#include "examples/example_utils.h"
#include "extras/ccsp_imageio.h"
#include "extras/extras.h"
#include "imageio/image_dec.h"
#include "imageio/image_enc.h"
#include "src/wp2/decode.h"
#include "src/wp2/encode.h"
using WP2::ExUtilGetFloat;
using WP2::ExUtilGetInt;
using WP2::ExUtilGetUInt;
using WP2::FileFormat;
using WP2::GetFileName;
using WP2::ProgramOptions;
using WP2::SPrintf;
//------------------------------------------------------------------------------
namespace {
void Help() {
ProgramOptions opt;
opt.Add("To and from .y4m tool.");
opt.Add("Usage:");
opt.Add(" ewp2 in_file [-o out_file] [options]");
opt.Add(
"If out_file has a .yuv extension, no y4m header is added and the "
"chroma sampling is 4:2:0.");
opt.Add("");
opt.Add("Options valid if 'out_file' has the .wp2 or .webp extension:");
opt.Add("-q <float>",
SPrintf("quality factor (0:small..100:big), default=%1.1f",
WP2::EncoderConfig::kDefault.quality));
opt.Add("-effort <int>",
SPrintf("compression effort (%d:fast..%d:slower/better), default=%d",
WP2::kMinEffort, WP2::kMaxEffort,
WP2::EncoderConfig::kDefault.effort));
opt.Add("-csp <int>", "color space (wp2 only)");
opt.Add("");
opt.Add(
"Options valid if 'out_file' has the .y4m extension "
"(4:4:4 8b by default):");
opt.Add("-y4m_depth <int>",
SPrintf("output bit depth per plane per sample (%u..%u)",
WP2::CCSPImageReader::kMinBitDepth,
WP2::CCSPImageReader::kMaxBitDepth));
opt.Add("-y4m_444", "4:4:4 output samples (full chroma resolution)");
opt.Add("-y4m_420", "4:2:0 output samples (half chroma resolution)");
opt.Add("");
opt.Add("Options valid if 'in_file' and/or 'out_file' are 4:2:0:");
opt.Add("-sampling <filter>",
"algorithm to use for up- and/or chroma downsampling: "
"smooth (default), nearest");
opt.Add("");
opt.Add("Options valid if 'out_file' is specified:");
opt.Add("-[no]metadata", "include [or exclude] input metadata in output");
opt.Add("-[psnr|ssim] <file path> <float>",
"error returned if output (or input if no given output) has a "
"distortion worse than specified compared to a reference file");
opt.Add("");
opt.Add("Other options:");
opt.Add("-v", "verbose");
opt.Add("-size", "print size and bits-per-pixel");
opt.Add("-rm [A|R|G|B|Y|U|V]",
"set specified input channel(s) values to opaque/black/grey");
opt.AddSystemOptionSection();
opt.Print();
}
enum ChannelId { kA, kR, kG, kB, kY, kU, kV, kNumChans };
void PrintSize(const std::string& file_path, size_t size, uint32_t num_pixels,
bool verbose, bool print_disto) {
const double bpp = size * 8. / num_pixels;
if (verbose) {
printf("%s %u bytes (%.3f bpp)\n", file_path.c_str(), (uint32_t)size, bpp);
} else {
printf("%s %u %.3f bpp%s", GetFileName(file_path).c_str(), (uint32_t)size,
bpp, print_disto ? "" : "\n");
}
}
} // namespace
//------------------------------------------------------------------------------
int main(int argc, char* argv[]) {
CHECK_TRUE(WP2CheckVersion(), "Error! Library version mismatch!");
if (argc < 2) {
fprintf(stderr, "Error: Missing arguments.\n");
Help();
return 0;
}
bool parsed_flag_q = false;
bool parsed_flag_effort = false;
bool parsed_flag_csp = false;
bool parsed_flag_y4m_depth = false;
bool parsed_flag_y4m_420 = false;
std::string in_path;
std::string out_path;
bool keep_metadata = true;
WP2::EncoderConfig wp2_enc_config;
// 4:4:4 8b by default
WP2::BitDepth y4m_enc_bit_depth = {8, /*is_signed=*/false};
uint32_t y4m_enc_420 = false;
const WP2::SamplingTaps* downsampling_filter = &WP2::SamplingTaps::kDownSharp;
const WP2::SamplingTaps* upsampling_filter = &WP2::SamplingTaps::kUpSmooth;
std::string ref_path;
WP2::MetricType ref_metric = WP2::NUM_METRIC_TYPES;
float ref_distortion = 99.f;
bool verbose = false, print_size = false;
bool rm_chan[kNumChans] = {};
for (int c = 1; c < argc; ++c) {
bool parse_error = false;
if (!strcmp(argv[c], "-h") || !strcmp(argv[c], "-help")) {
Help();
return 0;
} else if (!strcmp(argv[c], "-o") && c + 1 < argc) {
CHECK_TRUE(out_path.empty(), "Error! Output was already specified.");
out_path = argv[++c];
} else if (!strcmp(argv[c], "-q") && c + 1 < argc) {
parsed_flag_q = true;
wp2_enc_config.quality = ExUtilGetFloat(argv[++c], &parse_error);
} else if (!strcmp(argv[c], "-effort") && c + 1 < argc) {
parsed_flag_effort = true;
wp2_enc_config.effort = ExUtilGetInt(argv[++c], &parse_error);
} else if (!strcmp(argv[c], "-csp") && c + 1 < argc) {
parsed_flag_csp = true;
wp2_enc_config.csp_type = (WP2::Csp)ExUtilGetInt(argv[++c], &parse_error);
} else if (!strcmp(argv[c], "-y4m_depth") && c + 1 < argc) {
parsed_flag_y4m_depth = true;
y4m_enc_bit_depth = {ExUtilGetUInt(argv[++c], &parse_error),
/*is_signed=*/false};
CHECK_TRUE(
(y4m_enc_bit_depth.num_bits >= WP2::CCSPImageReader::kMinBitDepth &&
y4m_enc_bit_depth.num_bits <= WP2::CCSPImageReader::kMaxBitDepth),
"Error! Bit depth is outside allowed range.");
} else if (!strcmp(argv[c], "-y4m_444") || !strcmp(argv[c], "-y4m_420")) {
parsed_flag_y4m_420 = true;
y4m_enc_420 = !strcmp(argv[c], "-y4m_420");
} else if (!strcmp(argv[c], "-sampling") && c + 1 < argc) {
++c;
if (!strcmp(argv[c], "smooth")) {
downsampling_filter = &WP2::SamplingTaps::kDownSharp;
upsampling_filter = &WP2::SamplingTaps::kUpSmooth;
} else if (!strcmp(argv[c], "nearest")) {
downsampling_filter = &WP2::SamplingTaps::kDownAvg;
upsampling_filter = &WP2::SamplingTaps::kUpNearest;
} else {
CHECK_TRUE(false, "Error! Unsupported sampling '%s'", argv[c]);
}
} else if ((!strcmp(argv[c], "-psnr") || !strcmp(argv[c], "-ssim")) &&
c + 2 < argc) {
ref_metric = !strcmp(argv[c], "-psnr") ? WP2::PSNR : WP2::SSIM;
ref_path = argv[++c];
ref_distortion = ExUtilGetFloat(argv[++c], &parse_error);
} else if (!strcmp(argv[c], "-v")) {
verbose = true;
} else if (!strcmp(argv[c], "-size")) {
print_size = true;
} else if (!strcmp(argv[c], "-rm") && c + 1 < argc) {
constexpr char kChanChars[] = {'A', 'R', 'G', 'B', 'Y', 'U', 'V'};
STATIC_ASSERT_ARRAY_SIZE(kChanChars, kNumChans);
for (char character : std::string(argv[++c])) {
const char* const pos = std::strchr(kChanChars, character);
CHECK_TRUE(pos != nullptr, "Error! Unsupported -rm '%c'", character);
rm_chan[pos - kChanChars] = true;
}
} else if (!strcmp(argv[c], "-metadata")) {
keep_metadata = true;
} else if (!strcmp(argv[c], "-nometadata")) {
keep_metadata = false;
} else if (!strcmp(argv[c], "--") && c + 1 < argc) {
CHECK_TRUE(in_path.empty(), "Error! Input was already specified.");
in_path = argv[++c];
} else if (argv[c][0] == '-') {
bool must_stop;
CHECK_TRUE(ProgramOptions::ParseSystemOptions(argv[c], &must_stop),
"Error! Unknown option '%s'", argv[c]);
if (must_stop) return 0;
} else {
CHECK_TRUE(in_path.empty(), "Error! Input was already specified.");
in_path = argv[c];
}
if (parse_error) return 1;
}
const bool print_disto = !ref_path.empty();
CHECK_TRUE(!in_path.empty(), "Error! No input specified.");
CHECK_TRUE(wp2_enc_config.IsValid(), "Error! Invalid configuration.");
WP2::YUVPlane ccsp_buffer;
WP2::CSPMtx ccsp_to_rgb = {};
WP2::Metadata metadata;
WP2::Data in_data;
CHECK_STATUS(WP2::IoUtilReadFile(in_path.c_str(), &in_data),
"Error! Failed to read file '%s'.", in_path.c_str());
const FileFormat in_format =
WP2::GuessImageFormat(in_data.bytes, in_data.size);
FileFormat out_format = WP2::GetFormatFromExtension(out_path.c_str());
if (out_format == FileFormat::Y4M_420 || out_format == FileFormat::Y4M_444) {
out_format = (y4m_enc_420 ? FileFormat::Y4M_420 : FileFormat::Y4M_444);
}
if (parsed_flag_q &&
(out_format != FileFormat::WEBP && out_format != FileFormat::WP2)) {
printf("Warning: ignoring flag -q unsupported by output format %s\n",
WP2::GetFileFormatStr(out_format));
}
if (parsed_flag_effort &&
(out_format != FileFormat::WEBP && out_format != FileFormat::WP2)) {
printf("Warning: ignoring flag -effort unsupported by output format %s\n",
WP2::GetFileFormatStr(out_format));
}
if (parsed_flag_csp && out_format != FileFormat::WP2) {
printf("Warning: ignoring flag -csp unsupported by output format %s\n",
WP2::GetFileFormatStr(out_format));
}
if (parsed_flag_y4m_depth && (out_format != FileFormat::Y4M_420 &&
out_format != FileFormat::Y4M_444)) {
printf(
"Warning: ignoring flag -y4m_depth unsupported by output format %s\n",
WP2::GetFileFormatStr(out_format));
}
if (parsed_flag_y4m_420 && (out_format != FileFormat::Y4M_420 &&
out_format != FileFormat::Y4M_444)) {
printf(
"Warning: ignoring flag -y4m_444 or -y4m_420 unsupported by output "
"format %s\n",
WP2::GetFileFormatStr(out_format));
}
if (print_size) {
CHECK_TRUE(out_path != "-", "Error! Printing stdout size is unhandled.");
}
size_t output_file_size = 0;
size_t* const file_size_ptr = print_size ? &output_file_size : nullptr;
if (verbose) printf("Decoding '%s'\n", in_path.c_str());
if (in_format == FileFormat::WP2 && IsCustomColorSpace(out_format)) {
// Bypass conversion to and from RGB by decoding directly into YCbCr.
WP2::BitstreamFeatures features;
CHECK_STATUS(features.Read(in_data.bytes, in_data.size),
"Error! Failed to decode the wp2 image header.");
const uint32_t rgb_to_ycbcr_shift =
WP2::kRGBToYCbCrShift - (y4m_enc_bit_depth.num_bits - 8);
// Allocate a padded buffer to avoid doing it internally.
// TODO(yguyon): Check if 'has_alpha' is needed beforehand (decode GParams).
CHECK_STATUS(
ccsp_buffer.Resize(features.width, features.height,
/*pad=*/WP2::kPredWidth, /*has_alpha=*/true),
"Error! Failed to allocate buffer.");
CHECK_STATUS(
WP2::Decode(
in_data.bytes, in_data.size, WP2::kRGBToYCbCrMatrix,
rgb_to_ycbcr_shift,
ccsp_buffer.Y.Row(0), ccsp_buffer.Y.Step(), ccsp_buffer.Y.Size(),
ccsp_buffer.U.Row(0), ccsp_buffer.U.Step(), ccsp_buffer.U.Size(),
ccsp_buffer.V.Row(0), ccsp_buffer.V.Step(), ccsp_buffer.V.Size(),
ccsp_buffer.A.Row(0), ccsp_buffer.A.Step(), ccsp_buffer.A.Size(),
keep_metadata ? &metadata : nullptr),
"Error! Failed to decode the wp2 image.");
CHECK_TRUE(
!rm_chan[kR] && !rm_chan[kG] && !rm_chan[kB],
"Error! Input wp2 is decoded straight to YUV so -rm RGB is invalid.");
if (rm_chan[kA]) ccsp_buffer.A.Clear();
if (rm_chan[kY]) ccsp_buffer.Y.Fill(0);
if (rm_chan[kU]) ccsp_buffer.U.Fill(0);
if (rm_chan[kV]) ccsp_buffer.V.Fill(0);
// Remove padding.
WP2_ASSERT_STATUS(ccsp_buffer.SetView(
ccsp_buffer, {0, 0, features.width, features.height}));
if (WP2::IsChromaSubsampled(out_format)) {
CHECK_STATUS(ccsp_buffer.Downsample(*downsampling_filter),
"Error! Failed to downsample to 4:2:0.");
}
if (!out_path.empty()) {
if (verbose) printf("Saving to '%s'\n", out_path.c_str());
FILE* const fout = (out_path == "-") ? WP2::IoUtilSetBinaryMode(stdout)
: std::fopen(out_path.c_str(), "wb");
CHECK_TRUE(fout != nullptr, "Error! Failed to write file '%s'.",
out_path.c_str());
const WP2Status status =
WP2::WriteY4M(ccsp_buffer, y4m_enc_bit_depth, fout,
/*write_header=*/(out_format != FileFormat::YUV_420));
if (file_size_ptr != nullptr) *file_size_ptr = std::ftell(fout);
if (fout != stdout) fclose(fout); // Close file even if error.
CHECK_STATUS(status, "Error! Failed to write image '%s'.",
out_path.c_str());
if (!metadata.IsEmpty()) fprintf(stderr, "Warning: Ignored metadata.\n");
}
} else { // General case: any format -> RGB -> any format.
CHECK_STATUS(
WP2::ReadImage(in_data.bytes, in_data.size, &ccsp_buffer, &ccsp_to_rgb,
keep_metadata ? &metadata : nullptr),
"Error! Failed to read input '%s'.", in_path.c_str());
if (WP2::IsCustomColorSpace(in_format)) {
CHECK_TRUE(!rm_chan[kR] && !rm_chan[kG] && !rm_chan[kB],
"Error! Input is considered YUV so -rm RGB is invalid.");
} else {
CHECK_TRUE(!rm_chan[kY] && !rm_chan[kU] && !rm_chan[kV],
"Error! Input is considered RGB so -rm YUV is invalid.");
}
if (rm_chan[kA]) ccsp_buffer.A.Clear();
if (rm_chan[kY] || rm_chan[kR]) ccsp_buffer.Y.Fill(0);
if (rm_chan[kU] || rm_chan[kG]) ccsp_buffer.U.Fill(0);
if (rm_chan[kV] || rm_chan[kB]) ccsp_buffer.V.Fill(0);
if (!out_path.empty()) {
if (!WP2::IsChromaSubsampled(out_format) && ccsp_buffer.IsDownsampled()) {
CHECK_STATUS(ccsp_buffer.Upsample(*upsampling_filter),
"Error! Failed to upsample to 4:4:4.");
}
if (verbose) printf("Saving to '%s'\n", out_path.c_str());
if (out_format == FileFormat::WP2) {
WP2::MemoryWriter writer;
CHECK_STATUS(
WP2::Encode(ccsp_buffer.GetWidth(), ccsp_buffer.GetHeight(),
ccsp_buffer.Y.Row(0), ccsp_buffer.Y.Step(),
ccsp_buffer.U.Row(0), ccsp_buffer.U.Step(),
ccsp_buffer.V.Row(0), ccsp_buffer.V.Step(),
ccsp_buffer.HasAlpha() ? ccsp_buffer.A.Row(0) : nullptr,
ccsp_buffer.A.Step(), ccsp_to_rgb.mtx(),
ccsp_to_rgb.shift, &writer, wp2_enc_config, metadata),
"Error! Failed to encode as wp2.");
CHECK_STATUS(
WP2::IoUtilWriteFile(writer.mem_, writer.size_, out_path.c_str(),
/*overwrite=*/true, file_size_ptr),
"Error! Failed to write image '%s'.", out_path.c_str());
} else if (out_format == FileFormat::WEBP) {
WP2::ArgbBuffer argb_buffer;
CHECK_STATUS(ccsp_buffer.Export(ccsp_to_rgb, /*resize_if_needed=*/true,
&argb_buffer, upsampling_filter),
"Error! Failed to convert to Argb.");
if (!metadata.IsEmpty()) fprintf(stderr, "Warning: Ignored metadata\n");
WP2::MemoryWriter writer;
CHECK_STATUS(WP2::CompressWebP(argb_buffer, wp2_enc_config.quality,
wp2_enc_config.effort, &writer),
"Error! Failed to encode as WebP.");
CHECK_STATUS(
WP2::IoUtilWriteFile(writer.mem_, writer.size_, out_path.c_str(),
/*overwrite=*/true, file_size_ptr),
"Error! Failed to write image '%s'.", out_path.c_str());
} else {
const WP2::BitDepth file_num_bits =
(out_format == FileFormat::Y4M_444 ||
out_format == FileFormat::Y4M_420)
? y4m_enc_bit_depth
: WP2::BitDepth{8, /*is_signed=*/false};
CHECK_STATUS(
WP2::SaveImage(ccsp_buffer, ccsp_to_rgb, file_num_bits,
out_path.c_str(), /*overwrite=*/true, out_format,
metadata, *downsampling_filter, file_size_ptr),
"Error! Failed to save image '%s'.", out_path.c_str());
}
}
}
if (out_path.empty() && verbose) printf("No specified output.\n");
if (print_size) {
const uint32_t num_px = ccsp_buffer.GetWidth() * ccsp_buffer.GetHeight();
if (out_path.empty()) {
PrintSize(in_path, in_data.size, num_px, verbose, print_disto);
} else {
PrintSize(out_path, output_file_size, num_px, verbose, print_disto);
}
}
if (print_disto) {
const std::string& cmp_path = out_path.empty() ? in_path : out_path;
WP2::YUVPlane cmp, ref;
WP2::CSPMtx cmp_matrix = {}, ref_matrix = {};
if (cmp_path == in_path) { // Compare 'ref' with input.
using std::swap;
swap(ccsp_buffer, cmp);
cmp_matrix = ccsp_to_rgb;
} else { // Compare 'ref' with output.
CHECK_STATUS(
WP2::ReadImage(cmp_path.c_str(), &cmp, &cmp_matrix),
"Error! Failed to read '%s'.", cmp_path.c_str());
}
CHECK_STATUS(
WP2::ReadImage(ref_path.c_str(), &ref, &ref_matrix),
"Error! Failed to read reference '%s'.", ref_path.c_str());
CHECK_TRUE(
cmp_matrix == ref_matrix,
"Error! Can only compute distortion for the same color space.");
WP2::BitDepth cmp_bit_depth, ref_bit_depth;
CHECK_STATUS(WP2::ReadBitDepth(cmp_path.c_str(), &cmp_bit_depth),
"Error! Failed to read bit depth of '%s'.", cmp_path.c_str());
CHECK_STATUS(WP2::ReadBitDepth(ref_path.c_str(), &ref_bit_depth),
"Error! Failed to read bit depth of '%s'.", ref_path.c_str());
// Otherwise the matrix or the shift should have been different.
assert(cmp_bit_depth == ref_bit_depth);
if (cmp.HasAlpha() != ref.HasAlpha()) {
// Set image with missing alpha plane to opaque.
if (!cmp.HasAlpha()) {
CHECK_STATUS(cmp.A.Resize(cmp.GetWidth(), cmp.GetHeight()),
"Error! Allocation failure.");
cmp.A.Fill(WP2::kAlphaMax);
} else {
CHECK_STATUS(ref.A.Resize(ref.GetWidth(), ref.GetHeight()),
"Error! Allocation failure.");
ref.A.Fill(WP2::kAlphaMax);
}
}
float distortion[5];
CHECK_STATUS(cmp.GetDistortion(ref, ref_bit_depth, ref_metric, distortion),
"Error! Failed to compute distortion.");
if (verbose) {
printf("Distortion %f (alpha %f, %f, %f, %f) between '%s' and '%s'\n",
distortion[4], distortion[0], distortion[1], distortion[2],
distortion[3], cmp_path.c_str(), ref_path.c_str());
} else {
printf("%s distortion %f %f %f %f %f\n",
print_size ? " " : GetFileName(cmp_path).c_str(), distortion[4],
distortion[0], distortion[1], distortion[2], distortion[3]);
}
CHECK_TRUE(
distortion[4] >= ref_distortion,
"Error! Distortion %f from %s to %s is worse than the required %f.",
distortion[4], GetFileName(ref_path).c_str(),
GetFileName(cmp_path).c_str(), ref_distortion);
}
return 0;
}
//------------------------------------------------------------------------------