Skip to content

Commit

Permalink
tone_mapping: add new tone mapping API
Browse files Browse the repository at this point in the history
I was very unsatisfied with the old tone-mapping approach for a number
of reasons:

1. The requirement for all tone-mapping functions to be pure GLSL was
   very restrictive and made it so we had to start worrying about
   GPU performance.

2. For the more complicated functions, using a LUT is vastly better.
   Using a LUT also opens us up to using more complicated functions
   overall, like piecewise splines, including exact inverses of other
   tone mapping functions, or functions based on dynamic metadata.

3. Having this be an open-ended API in theory allows users to define
   custom tone mapping functions (e.g. perhaps one based on extra frame
   metadata).

4. Extensible enough to support e.g. inverse tone mapping as well.

I will probably still include GLSL fast paths for tone-mapping functions
that are faster to implement in pure GLSL, but for now I plan on
switching entirely to a LUT-based approach.

The major downside of this approach is that it doesn't afford us the
ability to combine this style of tone-mapping with full peak detection,
but I'm unsatisfied with the complexity of peak detection overall so I'm
not sure this is a huge loss in practice.

One way we can salvage the situation is by always using the detected
peak merely to (linearly) adjust the output of the LUT, e.g. something
like `sdr = lut(hdr) / lut(peak)`.

Closes https://code.videolan.org/videolan/libplacebo/-/issues/154
  • Loading branch information
haasn committed Dec 29, 2021
1 parent 9d50642 commit 32f02ce
Show file tree
Hide file tree
Showing 8 changed files with 815 additions and 11 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ libplacebo to take care of everything.
the needs of libplacebo, but may be useful to somebody else regardless. Also
contains the structs needed to define a filter kernel for the purposes of
libplacebo's upscaling routines.
- `tone_mapping.h`: A collection of tone mapping functions, used for
conversions between HDR and SDR content.

The API functions in this tier are either used throughout the program
(context, common etc.) or are low-level implementations of filter kernels,
Expand Down
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ project('libplacebo', ['c', 'cpp'],
'184': 'add pl_map_avframe/pl_unmap_avframe, deprecate pl_upload_avframe',
'185': 'add PL_COLOR_SYSTEM_DOLBYVISION and reshaping',
'186': 'add pl_d3d11_swapchain_params.flags',
'187': 'add <libplacebo/tone_mapping.h>',
}.keys().length(),
# Fix version
0)
Expand Down
2 changes: 2 additions & 0 deletions src/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
#include <libplacebo/shaders/lut.h>
#include <libplacebo/shaders/sampling.h>
#include <libplacebo/swapchain.h>
#include <libplacebo/tone_mapping.h>
#include <libplacebo/utils/frame_queue.h>
#include <libplacebo/utils/upload.h>

Expand Down Expand Up @@ -148,6 +149,7 @@ static inline float *pl_transpose(int dim, float *out, const float *in)
#define PL_DEF(x, d) ((x) ? (x) : (d))
#define PL_SQUARE(x) ((x) * (x))
#define PL_CUBE(x) ((x) * (x) * (x))
#define PL_MIX(a, b, x) ((x) * (b) + (1 - (x)) * (a))

// Helpers for doing alignment calculations
static inline size_t pl_gcd(size_t x, size_t y)
Expand Down
193 changes: 193 additions & 0 deletions src/include/libplacebo/tone_mapping.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* This file is part of libplacebo.
*
* libplacebo is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* libplacebo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with libplacebo. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef LIBPLACEBO_TONE_MAPPING_H_
#define LIBPLACEBO_TONE_MAPPING_H_

#include <stddef.h>
#include <stdbool.h>

#include <libplacebo/common.h>

PL_API_BEGIN

enum pl_hdr_scaling {
PL_HDR_NORM = 0, // 0.0 is absolute black, 1.0 is PL_COLOR_SDR_WHITE
PL_HDR_SQRT, // sqrt() of PL_HDR_NORM values
PL_HDR_NITS, // absolute brightness in raw cd/m²
PL_HDR_PQ, // absolute brightness in PQ (0.0 to 1.0)
PL_HDR_SCALING_COUNT,
};

// Generic helper for performing HDR scale conversions.
float pl_hdr_rescale(enum pl_hdr_scaling from, enum pl_hdr_scaling to, float x);

struct pl_tone_map_params;
struct pl_tone_map_function {
const char *name; // Identifier
const char *description; // Friendly / longer name

// If set, `pl_tone_map_params.param` can be adjusted to alter the
// characteristics of the tone mapping function in some way. (Optional)
const char *param_desc; // Name of parameter
float param_min;
float param_def;
float param_max;

// This controls the type of values input/output to/from `map`
enum pl_hdr_scaling scaling;

// The tone-mapping function itself. Iterates over all values in `lut`, and
// adapts them as needed.
//
// Note that the `params` struct fed into this function is guaranteed to
// satisfy `params->input_scaling == params->output_scaling == scaling`,
// and also obeys `params->input_max >= params->output_max`.
void (*map)(float *lut, const struct pl_tone_map_params *params);

// Inverse tone mapping function. Optional. If absent, this tone mapping
// curve only works in the forwards direction.
//
// For this function, `params->input_max <= params->output_max`.
void (*map_inverse)(float *lut, const struct pl_tone_map_params *params);

// Private data. Unused by libplacebo, but may be accessed by `map`.
void *priv;
};

struct pl_tone_map_params {
// If `function` is NULL, defaults to `pl_tone_map_clip`.
const struct pl_tone_map_function *function;
float param; // or 0.0 for default

// The desired input/output scaling of the tone map. If this differs from
// `function->scaling`, any required conversion will be performed.
//
// Note that to maximize LUT efficiency, it's *highly* recommended to use
// either PL_HDR_PQ or PL_HDR_SQRT as the input scaling, except when
// using `pl_tone_map_sample`.
enum pl_hdr_scaling input_scaling;
enum pl_hdr_scaling output_scaling;

// The size of the resulting LUT. (For `pl_tone_map_generate` only)
size_t lut_size;

// The characteristics of the input, in `input_scaling` units.
float input_min;
float input_max;

// The desired characteristics of the output, in `output_scaling` units.
float output_min;
float output_max;
};

#define pl_tone_map_params(...) (&(struct pl_tone_map_params) { __VA_ARGS__ });

// Note: Only does pointer equality testing on `function`
bool pl_tone_map_params_equal(const struct pl_tone_map_params *a,
const struct pl_tone_map_params *b);

// Returns true if the given tone mapping configuration effectively represents
// a no-op configuration. Tone mapping can be skipped in this case (although
// strictly speaking, the LUT would still clip illegal input values)
bool pl_tone_map_params_noop(const struct pl_tone_map_params *params);

// Generate a tone-mapping LUT for a given configuration. This will always
// span the entire input range, as given by `input_min` and `input_max`.
void pl_tone_map_generate(float *out, const struct pl_tone_map_params *params);

// Samples a tone mapping function at a single position. Note that this is less
// efficient than `pl_tone_map_generate` for generating multiple values.
//
// Ignores `params->lut_size`.
float pl_tone_map_sample(float x, const struct pl_tone_map_params *params);

// Special tone mapping function that means "automatically pick a good function
// based on the HDR levels". This is an opaque tone map function with no
// meaningful internal representation. (Besides `name` and `description`)
extern const struct pl_tone_map_function pl_tone_map_auto;

// Performs no tone-mapping, just clips out-of-range colors. Retains perfect
// color accuracy for in-range colors but completely destroys out-of-range
// information. Does not perform any black point adaptation.
extern const struct pl_tone_map_function pl_tone_map_clip;

// EETF from the ITU-R Report BT.2390, a hermite spline roll-off with linear
// segment. The knee point offset is configurable. Note that this defaults to
// 1.0, rather than the value of 0.5 from the ITU-R spec.
extern const struct pl_tone_map_function pl_tone_map_bt2390;

// EETF from ITU-R Report BT.2446, method A. Can be used for both forward
// and inverse tone mapping. Not configurable.
extern const struct pl_tone_map_function pl_tone_map_bt2446a;

// Simple spline consisting of two polynomials, joined by a single pivot point.
// The parameter gives the pivot point (in PQ space), defaulting to 0.30.
// Can be used for both forward and inverse tone mapping.
extern const struct pl_tone_map_function pl_tone_map_spline;

// Simple non-linear, global tone mapping algorithm. Named after Erik Reinhard.
// The parameter specifies the local contrast coefficient at the display peak.
// Essentially, a value of param=0.5 implies that the reference white will be
// about half as bright as when clipping. Defaults to 0.5, which results in the
// simplest formulation of this function.
extern const struct pl_tone_map_function pl_tone_map_reinhard;

// Generalization of the reinhard tone mapping algorithm to support an
// additional linear slope near black. The tone mapping parameter indicates the
// trade-off between the linear section and the non-linear section.
// Essentially, for param=0.5, every color value below 0.5 will be mapped
// linearly, with the higher values being non-linearly tone mapped. Values near
// 1.0 make this curve behave like pl_tone_map_clip, and values near 0.0 make
// this curve behave like pl_tone_map_reinhard. The default value is 0.3, which
// provides a good balance between colorimetric accuracy and preserving
// out-of-gamut details. The name is derived from its function shape
// (ax+b)/(cx+d), which is known as a Möbius transformation in mathematics.
extern const struct pl_tone_map_function pl_tone_map_mobius;

// Piece-wise, filmic tone-mapping algorithm developed by John Hable for use in
// Uncharted 2, inspired by a similar tone-mapping algorithm used by Kodak.
// Popularized by its use in video games with HDR rendering. Preserves both
// dark and bright details very well, but comes with the drawback of changing
// the average brightness quite significantly. This is sort of similar to
// pl_tone_map_reinhard with parameter 0.24.
extern const struct pl_tone_map_function pl_tone_map_hable;

// Fits a gamma (power) function to transfer between the source and target
// color spaces, effectively resulting in a perceptual hard-knee joining two
// roughly linear sections. This preserves details at all scales fairly
// accurately, but can result in an image with a muted or dull appearance. The
// parameter is used as the cutoff point, defaulting to 0.5.
extern const struct pl_tone_map_function pl_tone_map_gamma;

// Linearly stretches the input range to the output range, in PQ space. This
// will preserve all details accurately, but results in a significantly
// different average brightness. Can be used for inverse tone-mapping in
// addition to regular tone-mapping. The parameter can be used as an additional
// linear gain coefficient (defaulting to 1.0).
extern const struct pl_tone_map_function pl_tone_map_linear;

// A list of built-in tone mapping functions, terminated by NULL
extern const struct pl_tone_map_function * const pl_tone_map_functions[];
extern const int pl_num_tone_map_functions; // excluding trailing NULL

// Find the tone mapping function with the given name, or NULL on failure.
const struct pl_tone_map_function *pl_find_tone_map_function(const char *name);

PL_API_END

#endif // LIBPLACEBO_TONE_MAPPING_H_
3 changes: 3 additions & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ headers = [
'shaders/sampling.h',
'shaders.h',
'swapchain.h',
'tone_mapping.h',
'utils/dav1d.h',
'utils/dav1d_internal.h',
'utils/frame_queue.h',
Expand Down Expand Up @@ -222,6 +223,7 @@ sources = [
'shaders/lut.c',
'shaders/sampling.c',
'swapchain.c',
'tone_mapping.c',
'utils/frame_queue.c',
'utils/upload.c',
]
Expand All @@ -234,6 +236,7 @@ tests = [
'lut.c',
'filters.c',
'string.c',
'tone_mapping.c',
'utils.c',
]

Expand Down
17 changes: 6 additions & 11 deletions src/shaders/colorspace.c
Original file line number Diff line number Diff line change
Expand Up @@ -1144,14 +1144,6 @@ bool pl_get_detected_peak(const pl_shader_obj state,
return true;
}

static inline float pq_delinearize(float x)
{
x *= PL_COLOR_SDR_WHITE / 10000.0;
x = powf(x, PQ_M1);
x = (PQ_C1 + PQ_C2 * x) / (1.0 + PQ_C3 * x);
x = pow(x, PQ_M2);
return x;
}

const struct pl_color_map_params pl_color_map_default_params = { PL_COLOR_MAP_DEFAULTS };

Expand Down Expand Up @@ -1311,7 +1303,8 @@ static void pl_shader_tone_map(pl_shader sh, struct pl_color_space src,
10000 / PL_COLOR_SDR_WHITE, PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2);

// Normalize to be relative to the source brightness range
float pqlb = pq_delinearize(src.sig_floor * src.sig_scale);
float pqlb = pl_hdr_rescale(PL_HDR_NORM, PL_HDR_PQ,
src.sig_floor * src.sig_scale);
ident_t pqlb_c = SH_FLOAT(pqlb);
GLSL("float scale = 1.0 / (sig_pq.a - %s); \n"
"sig = clamp(vec3(scale) * (sig_pq.rgb - vec3(%s)), 0.0, 1.0); \n",
Expand All @@ -1327,7 +1320,8 @@ static void pl_shader_tone_map(pl_shader sh, struct pl_color_space src,
" (tb3 - 2.0 * tb2 + tb) * vec3(1.0 - ks) + \n"
" (-2.0 * tb3 + 3.0 * tb2) * vec3(maxLum); \n"
"sig = mix(sig, pb, %s(greaterThan(sig, vec3(ks)))); \n",
SH_FLOAT(pq_delinearize(dst.sig_peak * dst.sig_scale) - pqlb),
SH_FLOAT(pl_hdr_rescale(PL_HDR_NORM, PL_HDR_PQ,
dst.sig_peak * dst.sig_scale) - pqlb),
sh_bvec(sh, 3));
}
if (need_black) {
Expand All @@ -1338,7 +1332,8 @@ static void pl_shader_tone_map(pl_shader sh, struct pl_color_space src,
" p = min(1.0 / minLum, 4.0); \n"
"vec3 boost = vec3(minLum) * pow(vec3(1.0) - sig, vec3(p)); \n"
"vec3 sig_lift = sig + boost; \n",
SH_FLOAT(pq_delinearize(dst.sig_floor * dst.sig_scale) - pqlb));
SH_FLOAT(pl_hdr_rescale(PL_HDR_NORM, PL_HDR_PQ,
dst.sig_floor * dst.sig_scale) - pqlb));
if (need_peak) {
GLSL("if (maxLum < 1.0) { \n"
"sig_lift -= vec3(minLum); \n"
Expand Down
82 changes: 82 additions & 0 deletions src/tests/tone_mapping.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include "tests.h"
#include "log.h"

//#define PRINT_LUTS

int main()
{
pl_log log = pl_test_logger();

// PQ unit tests
REQUIRE(feq(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NITS, 0.0), 0.0, 1e-2));
REQUIRE(feq(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NITS, 1.0), 10000.0, 1e-2));
REQUIRE(feq(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NITS, 0.58), 203.0, 1e-2));

// Test round-trip
for (float x = 0.0f; x < 1.0f; x += 0.01f) {
REQUIRE(feq(x, pl_hdr_rescale(PL_HDR_NORM, PL_HDR_PQ,
pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, x)),
1e-5));
}

static float lut[128];
struct pl_tone_map_params params = {
.input_scaling = PL_HDR_PQ,
.output_scaling = PL_HDR_PQ,
.lut_size = PL_ARRAY_SIZE(lut),
};

// Test regular tone-mapping
params.input_min = pl_hdr_rescale(PL_HDR_NITS, params.input_scaling, 0.005);
params.input_max = pl_hdr_rescale(PL_HDR_NITS, params.input_scaling, 1000.0);
params.output_min = pl_hdr_rescale(PL_HDR_NORM, params.output_scaling, 0.001);
params.output_max = pl_hdr_rescale(PL_HDR_NORM, params.output_scaling, 1.0);

struct pl_tone_map_params params_inv = params;
PL_SWAP(params_inv.input_min, params_inv.output_min);
PL_SWAP(params_inv.input_max, params_inv.output_max);

// Generate example tone mapping curves, forward and inverse
for (int i = 0; i < pl_num_tone_map_functions; i++) {
const struct pl_tone_map_function *fun = pl_tone_map_functions[i];
if (fun == &pl_tone_map_auto)
continue;

printf("Testing tone-mapping function %s\n", fun->name);
params.function = params_inv.function = fun;
clock_t start = clock();
pl_tone_map_generate(lut, &params);
pl_log_cpu_time(log, start, clock(), "generating LUT");
for (int j = 0; j < PL_ARRAY_SIZE(lut); j++) {
REQUIRE(isfinite(lut[j]) && !isnan(lut[j]));
#ifdef PRINT_LUTS
printf("%f, %f\n", j / (PL_ARRAY_SIZE(lut) - 1.0f), lut[j]);
#endif
}

if (fun->map_inverse) {
start = clock();
pl_tone_map_generate(lut, &params_inv);
pl_log_cpu_time(log, start, clock(), "generating inverse LUT");
for (int j = 0; j < PL_ARRAY_SIZE(lut); j++) {
REQUIRE(isfinite(lut[j]) && !isnan(lut[j]));
#ifdef PRINT_LUTS
printf("%f, %f\n", j / (PL_ARRAY_SIZE(lut) - 1.0f), lut[j]);
#endif
}
}
}

// Test that `auto` is a no-op for 1:1 tone mapping
params.output_min = params.input_min;
params.output_max = params.input_max;
params.function = &pl_tone_map_auto;
pl_tone_map_generate(lut, &params);
for (int j = 0; j < PL_ARRAY_SIZE(lut); j++) {
float x = j / (PL_ARRAY_SIZE(lut) - 1.0f);
x = PL_MIX(params.input_min, params.input_max, x);
REQUIRE(feq(x, lut[j], 1e-5));
}

pl_log_destroy(&log);
}
Loading

0 comments on commit 32f02ce

Please sign in to comment.