blob: 6c72f46c49b01351772ec39292580dea74741bf6 [file] [log] [blame]
// Copyright 2023 Google LLC
// SPDX-License-Identifier: BSD-2-Clause
#include "tonemap_command.h"
#include <cmath>
#include "avif/avif_cxx.h"
#include "imageio.h"
namespace avif {
TonemapCommand::TonemapCommand()
: ProgramCommand("tonemap",
"Tone maps an avif image that has a gain map to a "
"given HDR headroom (how much brighter the display can go "
"compared to an SDR display)") {
argparse_.add_argument(arg_input_filename_, "input_image");
argparse_.add_argument(arg_output_filename_, "output_image");
argparse_.add_argument(arg_headroom_, "--headroom")
.help(
"HDR headroom to tone map to. This is log2 of the ratio of HDR to "
"SDR luminance. 0 means SDR.")
.default_value("0");
argparse_
.add_argument<CicpValues, CicpConverter>(arg_input_cicp_, "--cicp-input")
.help(
"Override input CICP values, expressed as P/T/M "
"where P = color primaries, T = transfer characteristics, "
"M = matrix coefficients.");
argparse_
.add_argument<CicpValues, CicpConverter>(arg_output_cicp_,
"--cicp-output")
.help(
"CICP values for the output, expressed as P/T/M "
"where P = color primaries, T = transfer characteristics, "
"M = matrix coefficients. P and M are only relevant when saving to "
"AVIF. "
"If not specified, 'color primaries' defaults to the base image's "
"primaries, 'transfer characteristics' defaults to 16 (PQ) if "
"headroom > 0, or 13 (sRGB) otherwise, 'matrix coefficients' "
"defaults to 6 (BT601).");
argparse_.add_argument(arg_clli_str_, "--clli")
.help(
"Override content light level information expressed as: "
"MaxCLL,MaxPALL. Only relevant when saving to AVIF.");
arg_image_read_.Init(argparse_);
arg_image_encode_.Init(argparse_, /*can_have_alpha=*/true);
}
avifResult TonemapCommand::Run() {
avifContentLightLevelInformationBox clli_box = {};
bool clli_set = false;
if (!arg_clli_str_.value().empty()) {
std::vector<uint16_t> clli;
if (!ParseList(arg_clli_str_, ',', 2, &clli)) {
std::cerr << "Invalid clli values, expected format: maxCLL,maxPALL where "
"both maxCLL and maxPALL are positive integers, got: "
<< arg_clli_str_ << "\n";
return AVIF_RESULT_INVALID_ARGUMENT;
}
clli_box.maxCLL = clli[0];
clli_box.maxPALL = clli[1];
clli_set = true;
}
const float headroom = arg_headroom_;
const bool tone_mapping_to_hdr = (headroom > 0.0f);
DecoderPtr decoder(avifDecoderCreate());
if (decoder == NULL) {
return AVIF_RESULT_OUT_OF_MEMORY;
}
decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
avifResult result = ReadAvif(decoder.get(), arg_input_filename_,
arg_image_read_.ignore_profile);
if (result != AVIF_RESULT_OK) {
return result;
}
avifImage* image = decoder->image;
if (image->gainMap == nullptr || image->gainMap->image == nullptr) {
std::cerr << "Input image " << arg_input_filename_
<< " does not contain a gain map\n";
return AVIF_RESULT_INVALID_ARGUMENT;
}
if (image->gainMap->baseHdrHeadroom.d == 0 ||
image->gainMap->alternateHdrHeadroom.d == 0) {
return AVIF_RESULT_INVALID_ARGUMENT;
}
const float base_hdr_hreadroom =
static_cast<float>(image->gainMap->baseHdrHeadroom.n) /
image->gainMap->baseHdrHeadroom.d;
const float alternate_hdr_hreadroom =
static_cast<float>(image->gainMap->alternateHdrHeadroom.n) /
image->gainMap->alternateHdrHeadroom.d;
// We are either tone mapping to the base image (i.e. leaving it as is),
// or tone mapping to the alternate image (i.e. fully applying the gain map),
// or tone mapping in between (partially applying the gain map).
const bool tone_mapping_to_base =
(headroom <= base_hdr_hreadroom &&
base_hdr_hreadroom <= alternate_hdr_hreadroom) ||
(headroom >= base_hdr_hreadroom &&
base_hdr_hreadroom >= alternate_hdr_hreadroom);
const bool tone_mapping_to_alternate =
(headroom <= alternate_hdr_hreadroom &&
alternate_hdr_hreadroom <= base_hdr_hreadroom) ||
(headroom >= alternate_hdr_hreadroom &&
alternate_hdr_hreadroom >= base_hdr_hreadroom);
const bool base_is_hdr = (base_hdr_hreadroom != 0.0f);
// Determine output CICP.
CicpValues cicp;
if (arg_output_cicp_.provenance() == argparse::Provenance::SPECIFIED) {
cicp = arg_output_cicp_; // User provided values.
} else if (tone_mapping_to_base || (tone_mapping_to_hdr && base_is_hdr)) {
cicp = {image->colorPrimaries, image->transferCharacteristics,
image->matrixCoefficients};
} else {
cicp = {image->gainMap->altColorPrimaries,
image->gainMap->altTransferCharacteristics,
image->gainMap->altMatrixCoefficients};
}
if (cicp.color_primaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED) {
// TODO(maryla): for now avifImageApplyGainMap always uses the primaries of
// the base image, but it should take into account the metadata's
// useBaseColorSpace property.
cicp.color_primaries = image->colorPrimaries;
}
if (cicp.transfer_characteristics ==
AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED) {
cicp.transfer_characteristics = static_cast<avifTransferCharacteristics>(
tone_mapping_to_hdr ? AVIF_TRANSFER_CHARACTERISTICS_PQ
: AVIF_TRANSFER_CHARACTERISTICS_SRGB);
}
// Determine output depth.
int depth = arg_image_read_.depth;
if (depth == 0) {
if (tone_mapping_to_base) {
depth = image->depth;
} else if (tone_mapping_to_alternate) {
depth = image->gainMap->altDepth;
}
if (depth == 0) {
// Default to the max depth between the base image, the gain map and
// the specified 'altDepth'.
depth = std::max(std::max(image->depth, image->gainMap->image->depth),
image->gainMap->altDepth);
}
}
// Determine output pixel format.
avifPixelFormat pixel_format =
(avifPixelFormat)arg_image_read_.pixel_format.value();
const avifPixelFormat alt_yuv_format =
(image->gainMap->altPlaneCount == 1)
? AVIF_PIXEL_FORMAT_YUV400
// Favor the least chroma subsampled format.
: std::min(image->yuvFormat, image->gainMap->image->yuvFormat);
if (pixel_format == AVIF_PIXEL_FORMAT_NONE) {
if (tone_mapping_to_base) {
pixel_format = image->yuvFormat;
} else if (tone_mapping_to_alternate) {
pixel_format = alt_yuv_format;
}
if (pixel_format == AVIF_PIXEL_FORMAT_NONE) {
// Default to the least chroma subsampled format provided.
pixel_format =
std::min(std::min(image->yuvFormat, image->gainMap->image->yuvFormat),
alt_yuv_format);
}
}
// Use the clli from the base image or the alternate image if the headroom
// is outside of the (baseHdrHeadroom, alternateHdrHeadroom) range.
if (!clli_set) {
if (tone_mapping_to_base) {
clli_box = image->clli;
} else if (tone_mapping_to_alternate) {
clli_box = image->gainMap->image->clli;
}
}
clli_set = (clli_box.maxCLL != 0) || (clli_box.maxPALL != 0);
ImagePtr tone_mapped(
avifImageCreate(image->width, image->height, depth, pixel_format));
if (tone_mapped == nullptr) {
return AVIF_RESULT_OUT_OF_MEMORY;
}
avifRGBImage tone_mapped_rgb;
avifRGBImageSetDefaults(&tone_mapped_rgb, tone_mapped.get());
avifDiagnostics diag;
result = avifImageApplyGainMap(
decoder->image, image->gainMap, arg_headroom_, cicp.color_primaries,
cicp.transfer_characteristics, &tone_mapped_rgb,
clli_set ? nullptr : &clli_box, &diag);
if (result != AVIF_RESULT_OK) {
std::cout << "Failed to tone map image: " << avifResultToString(result)
<< " (" << diag.error << ")\n";
return result;
}
result = avifImageRGBToYUV(tone_mapped.get(), &tone_mapped_rgb);
if (result != AVIF_RESULT_OK) {
std::cerr << "Failed to convert to YUV: " << avifResultToString(result)
<< "\n";
return result;
}
tone_mapped->clli = clli_box;
tone_mapped->transferCharacteristics = cicp.transfer_characteristics;
tone_mapped->colorPrimaries = cicp.color_primaries;
tone_mapped->matrixCoefficients = cicp.matrix_coefficients;
return WriteImage(tone_mapped.get(), arg_output_filename_,
arg_image_encode_.quality, arg_image_encode_.speed);
}
} // namespace avif