851 lines
32 KiB
C++
851 lines
32 KiB
C++
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
|
|
//
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
#include "tools/cjxl.h"
|
|
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "lib/extras/codec.h"
|
|
#if JPEGXL_ENABLE_JPEG
|
|
#include "lib/extras/codec_jpg.h"
|
|
#endif
|
|
|
|
#include "lib/extras/time.h"
|
|
#include "lib/jxl/aux_out.h"
|
|
#include "lib/jxl/base/cache_aligned.h"
|
|
#include "lib/jxl/base/compiler_specific.h"
|
|
#include "lib/jxl/base/data_parallel.h"
|
|
#include "lib/jxl/base/padded_bytes.h"
|
|
#include "lib/jxl/base/profiler.h"
|
|
#include "lib/jxl/base/status.h"
|
|
#include "lib/jxl/base/thread_pool_internal.h"
|
|
#include "lib/jxl/codec_in_out.h"
|
|
#include "lib/jxl/common.h"
|
|
#include "lib/jxl/enc_cache.h"
|
|
#include "lib/jxl/enc_file.h"
|
|
#include "lib/jxl/enc_params.h"
|
|
#include "lib/jxl/frame_header.h"
|
|
#include "lib/jxl/image.h"
|
|
#include "lib/jxl/image_bundle.h"
|
|
#include "lib/jxl/modular/encoding/encoding.h"
|
|
#include "tools/args.h"
|
|
#include "tools/box/box.h"
|
|
#include "tools/cpu/cpu.h"
|
|
#include "tools/speed_stats.h"
|
|
|
|
namespace jpegxl {
|
|
namespace tools {
|
|
namespace {
|
|
|
|
static inline bool ParseSpeedTier(const char* arg, jxl::SpeedTier* out) {
|
|
return jxl::ParseSpeedTier(arg, out);
|
|
}
|
|
static inline bool ParseColorTransform(const char* arg,
|
|
jxl::ColorTransform* out) {
|
|
size_t value = 0;
|
|
bool ret = ParseUnsigned(arg, &value);
|
|
if (ret && value > 2) ret = false;
|
|
if (ret) *out = jxl::ColorTransform(value);
|
|
return ret;
|
|
}
|
|
static inline bool ParseIntensityTarget(const char* arg, float* out) {
|
|
return ParseFloat(arg, out) && *out > 0;
|
|
}
|
|
static inline bool ParsePhotonNoiseParameter(const char* arg, float* out) {
|
|
return strncmp(arg, "ISO", 3) == 0 && ParseFloat(arg + 3, out) && *out > 0;
|
|
}
|
|
|
|
// Proposes a distance to try for a given bpp target. This could depend
|
|
// on the entropy in the image, too, but let's start with something.
|
|
static double ApproximateDistanceForBPP(double bpp) {
|
|
return 1.704 * pow(bpp, -0.804);
|
|
}
|
|
|
|
jxl::Status LoadSaliencyMap(const std::string& filename_heatmap,
|
|
jxl::ThreadPool* pool, jxl::ImageF* out_map) {
|
|
jxl::CodecInOut io_heatmap;
|
|
if (!SetFromFile(filename_heatmap, jxl::ColorHints(), &io_heatmap, pool)) {
|
|
return JXL_FAILURE("Could not load heatmap.");
|
|
}
|
|
*out_map = std::move(io_heatmap.Main().color()->Plane(0));
|
|
return true;
|
|
}
|
|
|
|
// Search algorithm for modular mode instead of Butteraugli distance.
|
|
void SetModularQualityForBitrate(jxl::ThreadPoolInternal* pool,
|
|
const size_t pixels, const double target_size,
|
|
CompressArgs* args) {
|
|
JXL_ASSERT(args->params.modular_mode);
|
|
|
|
CompressArgs s = *args; // Args for search.
|
|
float quality = -100 + target_size * 8.0 / pixels * 50;
|
|
if (quality > 100.f) quality = 100.f;
|
|
s.params.target_size = 0;
|
|
s.params.target_bitrate = 0;
|
|
double best_loss = 1e99;
|
|
float best_quality = quality;
|
|
float best_below = -10000.f;
|
|
float best_below_size = 0;
|
|
float best_above = 200.f;
|
|
float best_above_size = pixels * 15.f;
|
|
|
|
jxl::CodecInOut io;
|
|
double decode_mps = 0;
|
|
|
|
if (!LoadAll(*args, pool, &io, &decode_mps)) {
|
|
s.params.quality_pair = std::make_pair(quality, quality);
|
|
printf("couldn't load image\n");
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < 10; ++i) {
|
|
s.params.quality_pair = std::make_pair(quality, quality);
|
|
jxl::PaddedBytes candidate;
|
|
bool ok =
|
|
CompressJxl(io, decode_mps, pool, s, &candidate, /*print_stats=*/false);
|
|
if (!ok) {
|
|
printf(
|
|
"Compression error occurred during the search for best size."
|
|
" Trying with quality %.1f\n",
|
|
quality);
|
|
break;
|
|
}
|
|
printf("Quality %.2f yields %6zu bytes, %.3f bpp.\n", quality,
|
|
candidate.size(), candidate.size() * 8.0 / pixels);
|
|
const double ratio = static_cast<double>(candidate.size()) / target_size;
|
|
const double loss = std::abs(1.0 - ratio);
|
|
if (best_loss > loss) {
|
|
best_quality = quality;
|
|
best_loss = loss;
|
|
if (loss < 0.01f) break;
|
|
}
|
|
if (quality == 100.f && ratio < 1.f) break; // can't spend more bits
|
|
if (ratio > 1.f && quality < best_above) {
|
|
best_above = quality;
|
|
best_above_size = candidate.size();
|
|
}
|
|
if (ratio < 1.f && quality > best_below) {
|
|
best_below = quality;
|
|
best_below_size = candidate.size();
|
|
}
|
|
float t =
|
|
(target_size - best_below_size) / (best_above_size - best_below_size);
|
|
if (best_above > 100.f && ratio < 1.f) {
|
|
quality = (quality + 105) / 2;
|
|
} else if (best_above - best_below > 1000 && ratio > 1.f) {
|
|
quality -= 1000;
|
|
} else {
|
|
quality = best_above * t + best_below * (1.f - t);
|
|
}
|
|
if (quality >= 100.f) quality = 100.f;
|
|
}
|
|
args->params.quality_pair = std::make_pair(best_quality, best_quality);
|
|
args->params.target_bitrate = 0;
|
|
args->params.target_size = 0;
|
|
}
|
|
|
|
void SetParametersForSizeOrBitrate(jxl::ThreadPoolInternal* pool,
|
|
const size_t pixels, CompressArgs* args) {
|
|
CompressArgs s = *args; // Args for search.
|
|
|
|
// If fixed size, convert to bitrate.
|
|
if (s.params.target_size > 0) {
|
|
s.params.target_bitrate = s.params.target_size * 8.0 / pixels;
|
|
s.params.target_size = 0;
|
|
}
|
|
const double target_size = s.params.target_bitrate * (1 / 8.) * pixels;
|
|
|
|
if (args->params.modular_mode) {
|
|
SetModularQualityForBitrate(pool, pixels, target_size, args);
|
|
return;
|
|
}
|
|
|
|
double dist = ApproximateDistanceForBPP(s.params.target_bitrate);
|
|
s.params.target_bitrate = 0;
|
|
double best_dist = 1.0;
|
|
double best_loss = 1e99;
|
|
|
|
jxl::CodecInOut io;
|
|
double decode_mps = 0;
|
|
if (!LoadAll(*args, pool, &io, &decode_mps)) {
|
|
s.params.butteraugli_distance = static_cast<float>(dist);
|
|
printf("couldn't load image\n");
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < 7; ++i) {
|
|
s.params.butteraugli_distance = static_cast<float>(dist);
|
|
jxl::PaddedBytes candidate;
|
|
bool ok =
|
|
CompressJxl(io, decode_mps, pool, s, &candidate, /*print_stats=*/false);
|
|
if (!ok) {
|
|
printf(
|
|
"Compression error occurred during the search for best size. "
|
|
"Trying with butteraugli distance %.15g\n",
|
|
best_dist);
|
|
break;
|
|
}
|
|
printf("Butteraugli distance %.3f yields %6zu bytes, %.3f bpp.\n", dist,
|
|
candidate.size(), candidate.size() * 8.0 / pixels);
|
|
const double ratio = static_cast<double>(candidate.size()) / target_size;
|
|
const double loss = std::max(ratio, 1.0 / std::max(ratio, 1e-30));
|
|
if (best_loss > loss) {
|
|
best_dist = dist;
|
|
best_loss = loss;
|
|
}
|
|
dist *= ratio;
|
|
if (dist < 0.01) {
|
|
dist = 0.01;
|
|
}
|
|
if (dist >= 16.0) {
|
|
dist = 16.0;
|
|
}
|
|
}
|
|
args->params.butteraugli_distance = static_cast<float>(best_dist);
|
|
args->params.target_bitrate = 0;
|
|
args->params.target_size = 0;
|
|
}
|
|
|
|
const char* ModeFromArgs(const CompressArgs& args) {
|
|
if (args.jpeg_transcode) return "JPEG";
|
|
if (args.params.modular_mode) return "Modular";
|
|
return "VarDCT";
|
|
}
|
|
|
|
std::string QualityFromArgs(const CompressArgs& args) {
|
|
char buf[100];
|
|
if (args.jpeg_transcode) {
|
|
snprintf(buf, sizeof(buf), "lossless transcode");
|
|
} else if (args.params.modular_mode) {
|
|
if (args.params.quality_pair.first == 100 &&
|
|
args.params.quality_pair.second == 100) {
|
|
snprintf(buf, sizeof(buf), "lossless");
|
|
} else if (args.params.quality_pair.first !=
|
|
args.params.quality_pair.second) {
|
|
snprintf(buf, sizeof(buf), "Q%.2f,%.2f", args.params.quality_pair.first,
|
|
args.params.quality_pair.second);
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "Q%.2f", args.params.quality_pair.first);
|
|
}
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "d%.3f", args.params.butteraugli_distance);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
void PrintMode(jxl::ThreadPoolInternal* pool, const jxl::CodecInOut& io,
|
|
const double decode_mps, const CompressArgs& args) {
|
|
const char* mode = ModeFromArgs(args);
|
|
const char* speed = SpeedTierName(args.params.speed_tier);
|
|
const std::string quality = QualityFromArgs(args);
|
|
fprintf(stderr,
|
|
"Read %zux%zu image, %.1f MP/s\n"
|
|
"Encoding [%s%s, %s, %s",
|
|
io.xsize(), io.ysize(), decode_mps,
|
|
(args.use_container ? "Container | " : ""), mode, quality.c_str(),
|
|
speed);
|
|
if (args.use_container) {
|
|
if (args.jpeg_transcode) fprintf(stderr, " | JPEG reconstruction data");
|
|
if (!io.blobs.exif.empty())
|
|
fprintf(stderr, " | %zu-byte Exif", io.blobs.exif.size());
|
|
if (!io.blobs.xmp.empty())
|
|
fprintf(stderr, " | %zu-byte XMP", io.blobs.xmp.size());
|
|
if (!io.blobs.jumbf.empty())
|
|
fprintf(stderr, " | %zu-byte JUMBF", io.blobs.jumbf.size());
|
|
}
|
|
fprintf(stderr, "], %zu threads.\n", pool->NumWorkerThreads());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void CompressArgs::AddCommandLineOptions(CommandLineParser* cmdline) {
|
|
// Positional arguments.
|
|
cmdline->AddPositionalOption("INPUT", /* required = */ true,
|
|
"the input can be PNG"
|
|
#if JPEGXL_ENABLE_APNG
|
|
", APNG"
|
|
#endif
|
|
#if JPEGXL_ENABLE_GIF
|
|
", GIF"
|
|
#endif
|
|
#if JPEGXL_ENABLE_JPEG
|
|
", JPEG"
|
|
#endif
|
|
#if JPEGXL_ENABLE_EXR
|
|
", EXR"
|
|
#endif
|
|
", PPM, PFM, or PGX",
|
|
&file_in);
|
|
cmdline->AddPositionalOption(
|
|
"OUTPUT", /* required = */ true,
|
|
"the compressed JXL output file (can be omitted for benchmarking)",
|
|
&file_out);
|
|
|
|
// Flags.
|
|
// TODO(lode): also add options to add exif/xmp/other metadata in the
|
|
// container.
|
|
// TODO(lode): decide on good name for this flag: box, container, bmff, ...
|
|
cmdline->AddOptionFlag(
|
|
'\0', "container",
|
|
"Always encode using container format (default: only if needed)",
|
|
&use_container, &SetBooleanTrue, 1);
|
|
|
|
cmdline->AddOptionFlag('\0', "strip",
|
|
"Do not encode using container format (strips "
|
|
"Exif/XMP/JPEG bitstream reconstruction data)",
|
|
&no_container, &SetBooleanTrue, 2);
|
|
|
|
// Target distance/size/bpp
|
|
opt_distance_id = cmdline->AddOptionValue(
|
|
'd', "distance", "maxError",
|
|
("Max. butteraugli distance, lower = higher quality. Range: 0 .. 25.\n"
|
|
" 0.0 = mathematically lossless. Default for already-lossy input "
|
|
"(JPEG/GIF).\n"
|
|
" 1.0 = visually lossless. Default for other input.\n"
|
|
" Recommended range: 0.5 .. 3.0."),
|
|
¶ms.butteraugli_distance, &ParseFloat);
|
|
opt_target_size_id = cmdline->AddOptionValue(
|
|
'\0', "target_size", "N",
|
|
("Aim at file size of N bytes.\n"
|
|
" Compresses to 1 % of the target size in ideal conditions.\n"
|
|
" Runs the same algorithm as --target_bpp"),
|
|
¶ms.target_size, &ParseUnsigned, 1);
|
|
opt_target_bpp_id = cmdline->AddOptionValue(
|
|
'\0', "target_bpp", "BPP",
|
|
("Aim at file size that has N bits per pixel.\n"
|
|
" Compresses to 1 % of the target BPP in ideal conditions."),
|
|
¶ms.target_bitrate, &ParseFloat, 1);
|
|
|
|
// High-level options
|
|
opt_quality_id = cmdline->AddOptionValue(
|
|
'q', "quality", "QUALITY",
|
|
"Quality setting (is remapped to --distance). Range: -inf .. 100.\n"
|
|
" 100 = mathematically lossless. Default for already-lossy input "
|
|
"(JPEG/GIF).\n Positive quality values roughly match libjpeg quality.",
|
|
&quality, &ParseFloat);
|
|
|
|
cmdline->AddOptionValue(
|
|
'e', "effort", "EFFORT",
|
|
"Encoder effort setting. Range: 1 .. 9.\n"
|
|
" Default: 7. Higher number is more effort (slower).",
|
|
¶ms.speed_tier, &ParseSpeedTier);
|
|
|
|
cmdline->AddOptionValue(
|
|
's', "speed", "ANIMAL",
|
|
"Deprecated synonym for --effort. Valid values are:\n"
|
|
" lightning (1), thunder, falcon, cheetah, hare, wombat, squirrel, "
|
|
"kitten, tortoise (9)\n"
|
|
" Default: squirrel. Values are in order from faster to slower.\n",
|
|
¶ms.speed_tier, &ParseSpeedTier, 2);
|
|
|
|
cmdline->AddOptionValue('\0', "faster_decoding", "AMOUNT",
|
|
"Favour higher decoding speed. 0 = default, higher "
|
|
"values give higher speed at the expense of quality",
|
|
¶ms.decoding_speed_tier, &ParseUnsigned, 2);
|
|
|
|
cmdline->AddOptionFlag('p', "progressive",
|
|
"Enable progressive/responsive decoding.",
|
|
&progressive, &SetBooleanTrue);
|
|
|
|
cmdline->AddOptionFlag('\0', "premultiply",
|
|
"Force premultiplied (associated) alpha.",
|
|
&force_premultiplied, &SetBooleanTrue, 1);
|
|
cmdline->AddOptionValue('\0', "keep_invisible", "0|1",
|
|
"force disable/enable preserving color of invisible "
|
|
"pixels (default: 1 if lossless, 0 if lossy).",
|
|
¶ms.keep_invisible, &ParseOverride, 1);
|
|
|
|
cmdline->AddOptionFlag('\0', "centerfirst",
|
|
"Put center groups first in the compressed file.",
|
|
¶ms.centerfirst, &SetBooleanTrue, 1);
|
|
|
|
cmdline->AddOptionValue('\0', "center_x", "0..XSIZE",
|
|
"Put center groups first in the compressed file.",
|
|
¶ms.center_x, &ParseUnsigned, 1);
|
|
cmdline->AddOptionValue('\0', "center_y", "0..YSIZE",
|
|
"Put center groups first in the compressed file.",
|
|
¶ms.center_y, &ParseUnsigned, 1);
|
|
|
|
// Flags.
|
|
cmdline->AddOptionFlag('\0', "progressive_ac",
|
|
"Use the progressive mode for AC.",
|
|
¶ms.progressive_mode, &SetBooleanTrue, 1);
|
|
cmdline->AddOptionFlag('\0', "qprogressive_ac",
|
|
"Use the progressive mode for AC.",
|
|
¶ms.qprogressive_mode, &SetBooleanTrue, 1);
|
|
cmdline->AddOptionValue('\0', "progressive_dc", "num_dc_frames",
|
|
"Use progressive mode for DC.",
|
|
¶ms.progressive_dc, &ParseSigned, 1);
|
|
cmdline->AddOptionFlag('m', "modular",
|
|
"Use the modular mode (lossy / lossless).",
|
|
¶ms.modular_mode, &SetBooleanTrue, 1);
|
|
cmdline->AddOptionFlag('\0', "use_new_heuristics",
|
|
"use new and not yet ready encoder heuristics",
|
|
¶ms.use_new_heuristics, &SetBooleanTrue, 2);
|
|
|
|
// JPEG modes: parallel Brunsli, pixels to JPEG, or JPEG to Brunsli
|
|
cmdline->AddOptionFlag('j', "jpeg_transcode",
|
|
"Do lossy transcode of input JPEG file (decode to "
|
|
"pixels instead of doing lossless transcode).",
|
|
&jpeg_transcode, &SetBooleanFalse, 1);
|
|
|
|
opt_num_threads_id = cmdline->AddOptionValue(
|
|
'\0', "num_threads", "N", "number of worker threads (zero = none).",
|
|
&num_threads, &ParseUnsigned, 1);
|
|
cmdline->AddOptionValue('\0', "num_reps", "N", "how many times to compress.",
|
|
&num_reps, &ParseUnsigned, 1);
|
|
|
|
cmdline->AddOptionValue('\0', "noise", "0|1",
|
|
"force disable/enable noise generation.",
|
|
¶ms.noise, &ParseOverride, 1);
|
|
cmdline->AddOptionValue(
|
|
'\0', "photon_noise", "ISO3200",
|
|
"Set the noise to approximately what it would be at a given nominal "
|
|
"exposure on a 35mm camera. For formats other than 35mm, or when the "
|
|
"whole sensor was not used, you can multiply the ISO value by the "
|
|
"equivalence ratio squared, for example by 2.25 for an APS-C camera.",
|
|
¶ms.photon_noise_iso, &ParsePhotonNoiseParameter, 1);
|
|
cmdline->AddOptionValue('\0', "dots", "0|1",
|
|
"force disable/enable dots generation.", ¶ms.dots,
|
|
&ParseOverride, 1);
|
|
cmdline->AddOptionValue('\0', "patches", "0|1",
|
|
"force disable/enable patches generation.",
|
|
¶ms.patches, &ParseOverride, 1);
|
|
cmdline->AddOptionValue(
|
|
'\0', "resampling", "0|1|2|4|8",
|
|
"Subsample all color channels by this factor, or use 0 to choose the "
|
|
"resampling factor based on distance.",
|
|
¶ms.resampling, &ParseUnsigned, 0);
|
|
cmdline->AddOptionValue(
|
|
'\0', "ec_resampling", "1|2|4|8",
|
|
"Subsample all extra channels by this factor. If this value is smaller "
|
|
"than the resampling of color channels, it will be increased to match.",
|
|
¶ms.ec_resampling, &ParseUnsigned, 2);
|
|
cmdline->AddOptionFlag('\0', "already_downsampled",
|
|
"Do not downsample the given input before encoding, "
|
|
"but still signal that the decoder should upsample.",
|
|
¶ms.already_downsampled, &SetBooleanTrue, 2);
|
|
|
|
cmdline->AddOptionValue(
|
|
'\0', "epf", "-1..3",
|
|
"Edge preserving filter level (-1 = choose based on quality, default)",
|
|
¶ms.epf, &ParseSigned, 1);
|
|
|
|
cmdline->AddOptionValue('\0', "gaborish", "0|1",
|
|
"force disable/enable gaborish.", ¶ms.gaborish,
|
|
&ParseOverride, 1);
|
|
|
|
opt_intensity_target_id = cmdline->AddOptionValue(
|
|
'\0', "intensity_target", "N",
|
|
("Intensity target of monitor in nits, higher\n"
|
|
" results in higher quality image. Must be strictly positive.\n"
|
|
" Default is 255 for standard images, 4000 for input images known to\n"
|
|
" to have PQ or HLG transfer function."),
|
|
&intensity_target, &ParseIntensityTarget, 1);
|
|
|
|
cmdline->AddOptionValue('\0', "saliency_num_progressive_steps", "N", nullptr,
|
|
¶ms.saliency_num_progressive_steps,
|
|
&ParseUnsigned, 2);
|
|
cmdline->AddOptionValue('\0', "saliency_map_filename", "STRING", nullptr,
|
|
&saliency_map_filename, &ParseString, 2);
|
|
cmdline->AddOptionValue('\0', "saliency_threshold", "0..1", nullptr,
|
|
¶ms.saliency_threshold, &ParseFloat, 2);
|
|
|
|
cmdline->AddOptionValue(
|
|
'x', "dec-hints", "key=value",
|
|
"color_space indicates the ColorEncoding, see Description();\n"
|
|
"icc_pathname refers to a binary file containing an ICC profile.",
|
|
&color_hints, &ParseAndAppendKeyValue, 1);
|
|
|
|
cmdline->AddOptionValue(
|
|
'\0', "override_bitdepth", "0=use from image, 1-32=override",
|
|
"If nonzero, store the given bit depth in the JPEG XL file metadata"
|
|
" (1-32), instead of using the bit depth from the original input"
|
|
" image.",
|
|
&override_bitdepth, &ParseUnsigned, 2);
|
|
|
|
opt_color_id = cmdline->AddOptionValue(
|
|
'c', "colortransform", "0..2", "0=XYB, 1=None, 2=YCbCr",
|
|
¶ms.color_transform, &ParseColorTransform, 2);
|
|
|
|
// modular mode options
|
|
cmdline->AddOptionValue(
|
|
'Q', "mquality", "luma_q[,chroma_q]",
|
|
"[modular encoding] lossy 'quality' (100=lossless, lower is more lossy)",
|
|
¶ms.quality_pair, &ParseFloatPair, 1);
|
|
|
|
cmdline->AddOptionValue(
|
|
'I', "iterations", "F",
|
|
"[modular encoding] fraction of pixels used to learn MA trees "
|
|
"(default=0.5, try 0 for no MA and fast decode)",
|
|
¶ms.options.nb_repeats, &ParseFloat, 2);
|
|
|
|
cmdline->AddOptionValue(
|
|
'C', "colorspace", "K",
|
|
("[modular encoding] color transform: 0=RGB, 1=YCoCg, "
|
|
"2-37=RCT (default: try several, depending on speed)"),
|
|
¶ms.colorspace, &ParseSigned, 1);
|
|
|
|
opt_m_group_size_id = cmdline->AddOptionValue(
|
|
'g', "group-size", "K",
|
|
("[modular encoding] set group size to 128 << K "
|
|
"(default: 1 or 2)"),
|
|
¶ms.modular_group_size_shift, &ParseUnsigned, 1);
|
|
|
|
cmdline->AddOptionValue(
|
|
'P', "predictor", "K",
|
|
"[modular encoding] predictor(s) to use: 0=zero, "
|
|
"1=left, 2=top, 3=avg0, 4=select, 5=gradient, 6=weighted, "
|
|
"7=topright, 8=topleft, 9=leftleft, 10=avg1, 11=avg2, 12=avg3, "
|
|
"13=toptop predictive average "
|
|
"14=mix 5 and 6, 15=mix everything. Default 14, at slowest speed "
|
|
"default 15",
|
|
¶ms.options.predictor, &ParsePredictor, 1);
|
|
|
|
cmdline->AddOptionValue(
|
|
'E', "extra-properties", "K",
|
|
"[modular encoding] number of extra MA tree properties to use",
|
|
¶ms.options.max_properties, &ParseSigned, 2);
|
|
|
|
cmdline->AddOptionValue('\0', "palette", "K",
|
|
"[modular encoding] use a palette if image has at "
|
|
"most K colors (default: 1024)",
|
|
¶ms.palette_colors, &ParseSigned, 1);
|
|
|
|
cmdline->AddOptionFlag(
|
|
'\0', "lossy-palette",
|
|
"[modular encoding] quantize to a palette that has fewer entries than "
|
|
"would be necessary for perfect preservation; for the time being, it is "
|
|
"recommended to set --palette=0 with this option to use the default "
|
|
"palette only",
|
|
¶ms.lossy_palette, &SetBooleanTrue, 1);
|
|
|
|
cmdline->AddOptionValue(
|
|
'X', "pre-compact", "PERCENT",
|
|
("[modular encoding] compact channels (globally) if ratio "
|
|
"used/range is below this (default: 80%)"),
|
|
¶ms.channel_colors_pre_transform_percent, &ParseFloat, 2);
|
|
|
|
cmdline->AddOptionValue(
|
|
'Y', "post-compact", "PERCENT",
|
|
("[modular encoding] compact channels (per-group) if ratio "
|
|
"used/range is below this (default: 80%)"),
|
|
¶ms.channel_colors_percent, &ParseFloat, 2);
|
|
|
|
cmdline->AddOptionValue('R', "responsive", "K",
|
|
"[modular encoding] do Squeeze transform, 0=false, "
|
|
"1=true (default: true if lossy, false if lossless)",
|
|
¶ms.responsive, &ParseSigned, 1);
|
|
|
|
cmdline->AddOptionFlag('V', "version", "Print version number and exit",
|
|
&version, &SetBooleanTrue, 1);
|
|
cmdline->AddOptionFlag('\0', "quiet", "Be more silent", &quiet,
|
|
&SetBooleanTrue, 1);
|
|
cmdline->AddOptionValue('\0', "print_profile", "0|1",
|
|
"Print timing information before exiting",
|
|
&print_profile, &ParseOverride, 1);
|
|
|
|
cmdline->AddOptionFlag(
|
|
'v', "verbose",
|
|
"Verbose output; can be repeated, also applies to help (!).",
|
|
¶ms.verbose, &SetBooleanTrue);
|
|
}
|
|
|
|
jxl::Status CompressArgs::ValidateArgs(const CommandLineParser& cmdline) {
|
|
params.file_in = file_in;
|
|
params.file_out = file_out;
|
|
|
|
if (file_in == nullptr) {
|
|
fprintf(stderr, "Missing INPUT filename.\n");
|
|
return false;
|
|
}
|
|
|
|
bool got_distance = cmdline.GetOption(opt_distance_id)->matched();
|
|
bool got_target_size = cmdline.GetOption(opt_target_size_id)->matched();
|
|
bool got_target_bpp = cmdline.GetOption(opt_target_bpp_id)->matched();
|
|
bool got_quality = cmdline.GetOption(opt_quality_id)->matched();
|
|
bool got_intensity_target =
|
|
cmdline.GetOption(opt_intensity_target_id)->matched();
|
|
|
|
if (got_quality) {
|
|
default_settings = false;
|
|
if (quality < 100) jpeg_transcode = false;
|
|
// Quality settings roughly match libjpeg qualities.
|
|
if (quality < 7 || quality == 100 || params.modular_mode) {
|
|
if (jpeg_transcode == false) params.modular_mode = true;
|
|
// Internal modular quality to roughly match VarDCT size.
|
|
if (quality < 7) {
|
|
params.quality_pair.first = params.quality_pair.second =
|
|
std::min(35 + (quality - 7) * 3.0f, 100.0f);
|
|
} else {
|
|
params.quality_pair.first = params.quality_pair.second =
|
|
std::min(35 + (quality - 7) * 65.f / 93.f, 100.0f);
|
|
}
|
|
} else {
|
|
if (quality >= 30) {
|
|
params.butteraugli_distance = 0.1 + (100 - quality) * 0.09;
|
|
} else {
|
|
params.butteraugli_distance =
|
|
6.4 + pow(2.5, (30 - quality) / 5.0f) / 6.25f;
|
|
}
|
|
}
|
|
}
|
|
if (params.resampling > 1 && !params.already_downsampled)
|
|
jpeg_transcode = false;
|
|
|
|
if (progressive) {
|
|
params.qprogressive_mode = true;
|
|
params.responsive = 1;
|
|
default_settings = false;
|
|
}
|
|
if (got_target_size || got_target_bpp || got_intensity_target) {
|
|
default_settings = false;
|
|
}
|
|
|
|
if (params.progressive_dc < -1 || params.progressive_dc > 2) {
|
|
fprintf(stderr, "Invalid/out of range progressive_dc (%d), try -1 to 2.\n",
|
|
params.progressive_dc);
|
|
return false;
|
|
}
|
|
|
|
if (got_distance) {
|
|
constexpr float butteraugli_min_dist = 0.1f;
|
|
constexpr float butteraugli_max_dist = 25.0f;
|
|
if (!(0 <= params.butteraugli_distance &&
|
|
params.butteraugli_distance <= butteraugli_max_dist)) {
|
|
fprintf(stderr, "Invalid/out of range distance, try 0 to %g.\n",
|
|
butteraugli_max_dist);
|
|
return false;
|
|
}
|
|
if (params.butteraugli_distance > 0) jpeg_transcode = false;
|
|
if (params.butteraugli_distance == 0) {
|
|
// Use modular for lossless.
|
|
if (jpeg_transcode == false) params.modular_mode = true;
|
|
} else if (params.butteraugli_distance < butteraugli_min_dist) {
|
|
params.butteraugli_distance = butteraugli_min_dist;
|
|
}
|
|
default_settings = false;
|
|
}
|
|
|
|
if (got_target_bpp || got_target_size) {
|
|
jpeg_transcode = false;
|
|
}
|
|
|
|
if (got_target_bpp + got_target_size + got_distance + got_quality > 1) {
|
|
fprintf(stderr,
|
|
"You can specify only one of '--distance', '-q', "
|
|
"'--target_bpp' and '--target_size'. They are all different ways"
|
|
" to specify the image quality. When in doubt, use --distance."
|
|
" It gives the most visually consistent results.\n");
|
|
return false;
|
|
}
|
|
|
|
if (!saliency_map_filename.empty()) {
|
|
if (!params.progressive_mode) {
|
|
saliency_map_filename.clear();
|
|
fprintf(stderr,
|
|
"Warning: Specifying --saliency_map_filename only makes sense "
|
|
"for --progressive_ac mode.\n");
|
|
}
|
|
}
|
|
|
|
if (!params.file_in) {
|
|
fprintf(stderr, "Missing input filename.\n");
|
|
return false;
|
|
}
|
|
|
|
if (!cmdline.GetOption(opt_color_id)->matched()) {
|
|
// default to RGB for lossless modular
|
|
if (params.modular_mode) {
|
|
if (params.quality_pair.first != 100 ||
|
|
params.quality_pair.second != 100) {
|
|
params.color_transform = jxl::ColorTransform::kXYB;
|
|
} else {
|
|
params.color_transform = jxl::ColorTransform::kNone;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (override_bitdepth > 32) {
|
|
fprintf(stderr, "override_bitdepth must be <= 32\n");
|
|
return false;
|
|
}
|
|
|
|
if (params.epf > 3) {
|
|
fprintf(stderr, "--epf must be in the 0..3 range\n");
|
|
return false;
|
|
}
|
|
|
|
// User didn't override num_threads, so we have to compute a default, which
|
|
// might fail, so only do so when necessary. Don't just check num_threads != 0
|
|
// because the user may have set it to that.
|
|
if (!cmdline.GetOption(opt_num_threads_id)->matched()) {
|
|
cpu::ProcessorTopology topology;
|
|
if (!cpu::DetectProcessorTopology(&topology)) {
|
|
// We have seen sporadic failures caused by setaffinity_np.
|
|
fprintf(stderr,
|
|
"Failed to choose default num_threads; you can avoid this "
|
|
"error by specifying a --num_threads N argument.\n");
|
|
return false;
|
|
}
|
|
num_threads = topology.packages * topology.cores_per_package;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
jxl::Status CompressArgs::ValidateArgsAfterLoad(
|
|
const CommandLineParser& cmdline, const jxl::CodecInOut& io) {
|
|
if (!ValidateArgs(cmdline)) return false;
|
|
bool got_m_group_size = cmdline.GetOption(opt_m_group_size_id)->matched();
|
|
if (params.modular_mode && !got_m_group_size) {
|
|
// Default modular group size: set to 512 if 256 would be silly
|
|
const size_t kThinImageThr = 256 + 64;
|
|
const size_t kSmallImageThr = 256 + 128;
|
|
if (io.xsize() < kThinImageThr || io.ysize() < kThinImageThr ||
|
|
(io.xsize() < kSmallImageThr && io.ysize() < kSmallImageThr)) {
|
|
params.modular_group_size_shift = 2;
|
|
}
|
|
}
|
|
if (!io.blobs.exif.empty() || !io.blobs.xmp.empty() ||
|
|
!io.blobs.jumbf.empty() || !io.blobs.iptc.empty() || jpeg_transcode) {
|
|
use_container = true;
|
|
}
|
|
if (no_container) use_container = false;
|
|
if (jpeg_transcode && params.modular_mode) {
|
|
fprintf(stderr,
|
|
"Error: cannot do lossless JPEG transcode in modular mode.\n");
|
|
return false;
|
|
}
|
|
if (jpeg_transcode) {
|
|
if (params.progressive_mode || params.qprogressive_mode ||
|
|
params.progressive_dc > 0) {
|
|
fprintf(stderr,
|
|
"Error: progressive lossless JPEG transcode is not yet "
|
|
"implemented.\n");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
jxl::Status LoadAll(CompressArgs& args, jxl::ThreadPoolInternal* pool,
|
|
jxl::CodecInOut* io, double* decode_mps) {
|
|
const double t0 = jxl::Now();
|
|
|
|
io->target_nits = args.intensity_target;
|
|
io->dec_target = (args.jpeg_transcode ? jxl::DecodeTarget::kQuantizedCoeffs
|
|
: jxl::DecodeTarget::kPixels);
|
|
jxl::Codec input_codec;
|
|
if (!SetFromFile(args.params.file_in, args.color_hints, io, nullptr,
|
|
&input_codec)) {
|
|
fprintf(stderr, "Failed to read image %s.\n", args.params.file_in);
|
|
return false;
|
|
}
|
|
if (input_codec != jxl::Codec::kJPG) args.jpeg_transcode = false;
|
|
if (args.jpeg_transcode) args.params.butteraugli_distance = 0;
|
|
|
|
if (input_codec == jxl::Codec::kGIF && args.default_settings) {
|
|
args.params.modular_mode = true;
|
|
args.params.quality_pair.first = args.params.quality_pair.second = 100;
|
|
}
|
|
if (args.override_bitdepth != 0) {
|
|
if (args.override_bitdepth == 32) {
|
|
io->metadata.m.SetFloat32Samples();
|
|
} else {
|
|
io->metadata.m.SetUintSamples(args.override_bitdepth);
|
|
}
|
|
}
|
|
if (args.force_premultiplied) {
|
|
io->PremultiplyAlpha();
|
|
}
|
|
|
|
jxl::ImageF saliency_map;
|
|
if (!args.saliency_map_filename.empty()) {
|
|
if (!LoadSaliencyMap(args.saliency_map_filename, pool, &saliency_map)) {
|
|
fprintf(stderr, "Failed to read saliency map %s.\n",
|
|
args.saliency_map_filename.c_str());
|
|
return false;
|
|
}
|
|
args.params.saliency_map = &saliency_map;
|
|
}
|
|
|
|
const double t1 = jxl::Now();
|
|
const size_t pixels = io->xsize() * io->ysize();
|
|
*decode_mps = pixels * io->frames.size() * 1E-6 / (t1 - t0);
|
|
|
|
return true;
|
|
}
|
|
|
|
jxl::Status CompressJxl(jxl::CodecInOut& io, double decode_mps,
|
|
jxl::ThreadPoolInternal* pool, CompressArgs& args,
|
|
jxl::PaddedBytes* compressed, bool print_stats) {
|
|
JXL_CHECK(pool);
|
|
|
|
const size_t pixels = io.xsize() * io.ysize();
|
|
|
|
if (args.params.target_size > 0 || args.params.target_bitrate > 0) {
|
|
// Slow iterative search for parameters that reach target bpp / size.
|
|
SetParametersForSizeOrBitrate(pool, pixels, &args);
|
|
}
|
|
|
|
if (print_stats) PrintMode(pool, io, decode_mps, args);
|
|
|
|
// Final/actual compression run (possibly repeated for benchmarking).
|
|
jxl::AuxOut aux_out;
|
|
if (args.inspector_image3f) {
|
|
aux_out.SetInspectorImage3F(args.inspector_image3f);
|
|
}
|
|
SpeedStats stats;
|
|
jxl::PassesEncoderState passes_encoder_state;
|
|
if (args.params.use_new_heuristics) {
|
|
passes_encoder_state.heuristics =
|
|
jxl::make_unique<jxl::FastEncoderHeuristics>();
|
|
}
|
|
for (size_t i = 0; i < args.num_reps; ++i) {
|
|
const double t0 = jxl::Now();
|
|
jxl::Status ok = false;
|
|
if (io.Main().IsJPEG()) {
|
|
// TODO(lode): automate this in the encoder. The encoder must in the
|
|
// beginning choose to either do all in xyb, or all in non-xyb, write
|
|
// that in the xyb_encoded header flag, and persistently keep that state
|
|
// to check if every frame uses an allowed color transform.
|
|
args.params.color_transform = io.Main().color_transform;
|
|
}
|
|
ok = EncodeFile(args.params, &io, &passes_encoder_state, compressed,
|
|
&aux_out, pool);
|
|
if (!ok) {
|
|
fprintf(stderr, "Failed to compress to %s.\n", ModeFromArgs(args));
|
|
return false;
|
|
}
|
|
const double t1 = jxl::Now();
|
|
stats.NotifyElapsed(t1 - t0);
|
|
stats.SetImageSize(io.xsize(), io.ysize());
|
|
}
|
|
|
|
if (print_stats) {
|
|
const double bpp =
|
|
static_cast<double>(compressed->size() * jxl::kBitsPerByte) / pixels;
|
|
fprintf(stderr, "Compressed to %zu bytes (%.3f bpp%s).\n",
|
|
compressed->size(), bpp / io.frames.size(),
|
|
io.frames.size() == 1 ? "" : "/frame");
|
|
JXL_CHECK(stats.Print(args.num_threads));
|
|
if (args.params.verbose) {
|
|
aux_out.Print(1);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace tools
|
|
} // namespace jpegxl
|