tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

photon_noise_table.c (17346B)


      1 /*
      2 * Copyright (c) 2021, Alliance for Open Media. All rights reserved.
      3 *
      4 * This source code is subject to the terms of the BSD 2 Clause License and
      5 * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
      6 * was not distributed with this source code in the LICENSE file, you can
      7 * obtain it at www.aomedia.org/license/software. If the Alliance for Open
      8 * Media Patent License 1.0 was not distributed with this source code in the
      9 * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
     10 */
     11 
     12 // This tool creates a film grain table, for use in stills and videos,
     13 // representing the noise that one would get by shooting with a digital camera
     14 // at a given light level. Much of the noise in digital images is photon shot
     15 // noise, which is due to the characteristics of photon arrival and grows in
     16 // standard deviation as the square root of the expected number of photons
     17 // captured.
     18 // https://www.photonstophotos.net/Emil%20Martinec/noise.html#shotnoise
     19 //
     20 // The proxy used by this tool for the amount of light captured is the ISO value
     21 // such that the focal plane exposure at the time of capture would have been
     22 // mapped by a 35mm camera to the output lightness observed in the image. That
     23 // is, if one were to shoot on a 35mm camera (36×24mm sensor) at the nominal
     24 // exposure for that ISO setting, the resulting image should contain noise of
     25 // the same order of magnitude as generated by this tool.
     26 //
     27 // Example usage:
     28 //
     29 //     ./photon_noise_table --width=3840 --height=2160 --iso=25600 -o noise.tbl
     30 //     # Then, for example:
     31 //     aomenc --film-grain-table=noise.tbl ...
     32 //     # Or:
     33 //     avifenc -c aom -a film-grain-table=noise.tbl ...
     34 //
     35 // The (mostly) square-root relationship between light intensity and noise
     36 // amplitude holds in linear light, but AV1 streams are most often encoded
     37 // non-linearly, and the film grain is applied to those non-linear values.
     38 // Therefore, this tool must account for the non-linearity, and this is
     39 // controlled by the optional `--transfer-function` (or `-t`) parameter, which
     40 // specifies the tone response curve that will be used when encoding the actual
     41 // image. The default for this tool is sRGB, which is approximately similar to
     42 // an encoding gamma of 1/2.2 (i.e. a decoding gamma of 2.2) though not quite
     43 // identical.
     44 //
     45 // As alluded to above, the tool assumes that the image is taken from the
     46 // entirety of a 36×24mm (“35mm format”) sensor. If that assumption does not
     47 // hold, then a “35mm-equivalent ISO value” that can be passed to the tool can
     48 // be obtained by multiplying the true ISO value by the ratio of 36×24mm to the
     49 // area that was actually used. For formats that approximately share the same
     50 // aspect ratio, this is often expressed as the square of the “equivalence
     51 // ratio” which is the ratio of their diagonals. For example, APS-C (often
     52 // ~24×16mm) is said to have an equivalence ratio of 1.5 relative to the 35mm
     53 // format, and therefore ISO 1000 on APS-C and ISO 1000×1.5² = 2250 on 35mm
     54 // produce an image of the same lightness from the same amount of light spread
     55 // onto their respective surface areas (resulting in different focal plane
     56 // exposures), and those images will thus have similar amounts of noise if the
     57 // cameras are of similar technology. https://doi.org/10.1117/1.OE.57.11.110801
     58 //
     59 // The tool needs to know the resolution of the images to which its grain tables
     60 // will be applied so that it can know how the light on the sensor was shared
     61 // between its pixels. As a general rule, while a higher pixel count will lead
     62 // to more noise per pixel, when the final image is viewed at the same physical
     63 // size, that noise will tend to “average out” to the same amount over a given
     64 // area, since there will be more pixels in it which, in aggregate, will have
     65 // received essentially as much light. Put differently, the amount of noise
     66 // depends on the scale at which it is measured, and the decision for this tool
     67 // was to make that scale relative to the image instead of its constituent
     68 // samples. For more on this, see:
     69 //
     70 // https://www.photonstophotos.net/Emil%20Martinec/noise-p3.html#pixelsize
     71 // https://www.dpreview.com/articles/5365920428/the-effect-of-pixel-and-sensor-sizes-on-noise/2
     72 // https://www.dpreview.com/videos/7940373140/dpreview-tv-why-lower-resolution-sensors-are-not-better-in-low-light
     73 
     74 #include <math.h>
     75 #include <stdio.h>
     76 #include <stdlib.h>
     77 #include <string.h>
     78 
     79 #include "aom_dsp/grain_table.h"
     80 #include "common/args.h"
     81 #include "common/tools_common.h"
     82 
     83 static const char *exec_name;
     84 
     85 static const struct arg_enum_list transfer_functions[] = {
     86  { "bt470m", AOM_CICP_TC_BT_470_M }, { "bt470bg", AOM_CICP_TC_BT_470_B_G },
     87  { "srgb", AOM_CICP_TC_SRGB },       { "smpte2084", AOM_CICP_TC_SMPTE_2084 },
     88  { "hlg", AOM_CICP_TC_HLG },         ARG_ENUM_LIST_END
     89 };
     90 
     91 static arg_def_t help_arg =
     92    ARG_DEF("h", "help", 0, "Show the available options");
     93 static arg_def_t width_arg =
     94    ARG_DEF("w", "width", 1, "Width of the image in pixels (required)");
     95 static arg_def_t height_arg =
     96    ARG_DEF("l", "height", 1, "Height of the image in pixels (required)");
     97 static arg_def_t iso_arg = ARG_DEF(
     98    "i", "iso", 1, "ISO setting indicative of the light level (required)");
     99 static arg_def_t output_arg =
    100    ARG_DEF("o", "output", 1,
    101            "Output file to which to write the film grain table (required)");
    102 static arg_def_t transfer_function_arg =
    103    ARG_DEF_ENUM("t", "transfer-function", 1,
    104                 "Transfer function used by the encoded image (default = sRGB)",
    105                 transfer_functions);
    106 
    107 void usage_exit(void) {
    108  fprintf(stderr,
    109          "Usage: %s [--transfer-function=<tf>] --width=<width> "
    110          "--height=<height> --iso=<iso> --output=<output.tbl>\n",
    111          exec_name);
    112  exit(EXIT_FAILURE);
    113 }
    114 
    115 typedef struct {
    116  float (*to_linear)(float);
    117  float (*from_linear)(float);
    118  // In linear output light. This would typically be 0.18 for SDR (this matches
    119  // the definition of Standard Output Sensitivity from ISO 12232:2019), but in
    120  // HDR, we certainly do not want to consider 18% of the maximum output a
    121  // “mid-tone”, as it would be e.g. 1800 cd/m² for SMPTE ST 2084 (PQ).
    122  float mid_tone;
    123 } transfer_function_t;
    124 
    125 static const transfer_function_t *find_transfer_function(
    126    aom_transfer_characteristics_t tc);
    127 
    128 typedef struct {
    129  int width;
    130  int height;
    131  int iso_setting;
    132 
    133  const transfer_function_t *transfer_function;
    134 
    135  const char *output_filename;
    136 } photon_noise_args_t;
    137 
    138 static void parse_args(int argc, char **argv,
    139                       photon_noise_args_t *photon_noise_args) {
    140  static const arg_def_t *args[] = { &help_arg,   &width_arg,
    141                                     &height_arg, &iso_arg,
    142                                     &output_arg, &transfer_function_arg,
    143                                     NULL };
    144  struct arg arg;
    145  int width_set = 0, height_set = 0, iso_set = 0, output_set = 0, i;
    146 
    147  photon_noise_args->transfer_function =
    148      find_transfer_function(AOM_CICP_TC_SRGB);
    149 
    150  for (i = 1; i < argc; i += arg.argv_step) {
    151    arg.argv_step = 1;
    152    if (arg_match(&arg, &help_arg, argv + i)) {
    153      arg_show_usage(stdout, args);
    154      exit(EXIT_SUCCESS);
    155    } else if (arg_match(&arg, &width_arg, argv + i)) {
    156      photon_noise_args->width = arg_parse_int(&arg);
    157      width_set = 1;
    158    } else if (arg_match(&arg, &height_arg, argv + i)) {
    159      photon_noise_args->height = arg_parse_int(&arg);
    160      height_set = 1;
    161    } else if (arg_match(&arg, &iso_arg, argv + i)) {
    162      photon_noise_args->iso_setting = arg_parse_int(&arg);
    163      iso_set = 1;
    164    } else if (arg_match(&arg, &output_arg, argv + i)) {
    165      photon_noise_args->output_filename = arg.val;
    166      output_set = 1;
    167    } else if (arg_match(&arg, &transfer_function_arg, argv + i)) {
    168      const aom_transfer_characteristics_t tc = arg_parse_enum(&arg);
    169      photon_noise_args->transfer_function = find_transfer_function(tc);
    170    } else {
    171      fatal("unrecognized argument \"%s\", see --help for available options",
    172            argv[i]);
    173    }
    174  }
    175 
    176  if (!width_set) {
    177    fprintf(stderr, "Missing required parameter --width\n");
    178    exit(EXIT_FAILURE);
    179  }
    180 
    181  if (!height_set) {
    182    fprintf(stderr, "Missing required parameter --height\n");
    183    exit(EXIT_FAILURE);
    184  }
    185 
    186  if (!iso_set) {
    187    fprintf(stderr, "Missing required parameter --iso\n");
    188    exit(EXIT_FAILURE);
    189  }
    190 
    191  if (!output_set) {
    192    fprintf(stderr, "Missing required parameter --output\n");
    193    exit(EXIT_FAILURE);
    194  }
    195 }
    196 
    197 static float maxf(float a, float b) { return a > b ? a : b; }
    198 static float minf(float a, float b) { return a < b ? a : b; }
    199 
    200 static float gamma22_to_linear(float g) { return powf(g, 2.2f); }
    201 static float gamma22_from_linear(float l) { return powf(l, 1 / 2.2f); }
    202 static float gamma28_to_linear(float g) { return powf(g, 2.8f); }
    203 static float gamma28_from_linear(float l) { return powf(l, 1 / 2.8f); }
    204 
    205 static float srgb_to_linear(float srgb) {
    206  return srgb <= 0.04045f ? srgb / 12.92f
    207                          : powf((srgb + 0.055f) / 1.055f, 2.4f);
    208 }
    209 static float srgb_from_linear(float linear) {
    210  return linear <= 0.0031308f ? 12.92f * linear
    211                              : 1.055f * powf(linear, 1 / 2.4f) - 0.055f;
    212 }
    213 
    214 static const float kPqM1 = 2610.f / 16384;
    215 static const float kPqM2 = 128 * 2523.f / 4096;
    216 static const float kPqC1 = 3424.f / 4096;
    217 static const float kPqC2 = 32 * 2413.f / 4096;
    218 static const float kPqC3 = 32 * 2392.f / 4096;
    219 static float pq_to_linear(float pq) {
    220  const float pq_pow_inv_m2 = powf(pq, 1.f / kPqM2);
    221  return powf(maxf(0, pq_pow_inv_m2 - kPqC1) / (kPqC2 - kPqC3 * pq_pow_inv_m2),
    222              1.f / kPqM1);
    223 }
    224 static float pq_from_linear(float linear) {
    225  const float linear_pow_m1 = powf(linear, kPqM1);
    226  return powf((kPqC1 + kPqC2 * linear_pow_m1) / (1 + kPqC3 * linear_pow_m1),
    227              kPqM2);
    228 }
    229 
    230 // Note: it is perhaps debatable whether “linear” for HLG should be scene light
    231 // or display light. Here, it is implemented in terms of display light assuming
    232 // a nominal peak display luminance of 1000 cd/m², hence the system γ of 1.2. To
    233 // make it scene light instead, the OOTF (powf(x, 1.2f)) and its inverse should
    234 // be removed from the functions below, and the .mid_tone should be replaced
    235 // with powf(26.f / 1000, 1 / 1.2f).
    236 static const float kHlgA = 0.17883277f;
    237 static const float kHlgB = 0.28466892f;
    238 static const float kHlgC = 0.55991073f;
    239 static float hlg_to_linear(float hlg) {
    240  // EOTF = OOTF ∘ OETF⁻¹
    241  const float linear =
    242      hlg <= 0.5f ? hlg * hlg / 3 : (expf((hlg - kHlgC) / kHlgA) + kHlgB) / 12;
    243  return powf(linear, 1.2f);
    244 }
    245 static float hlg_from_linear(float linear) {
    246  // EOTF⁻¹ = OETF ∘ OOTF⁻¹
    247  linear = powf(linear, 1.f / 1.2f);
    248  return linear <= 1.f / 12 ? sqrtf(3 * linear)
    249                            : kHlgA * logf(12 * linear - kHlgB) + kHlgC;
    250 }
    251 
    252 static const transfer_function_t *find_transfer_function(
    253    aom_transfer_characteristics_t tc) {
    254  static const transfer_function_t
    255      kGamma22TransferFunction = { .to_linear = &gamma22_to_linear,
    256                                   .from_linear = &gamma22_from_linear,
    257                                   .mid_tone = 0.18f },
    258      kGamma28TransferFunction = { .to_linear = &gamma28_to_linear,
    259                                   .from_linear = &gamma28_from_linear,
    260                                   .mid_tone = 0.18f },
    261      kSRgbTransferFunction = { .to_linear = &srgb_to_linear,
    262                                .from_linear = &srgb_from_linear,
    263                                .mid_tone = 0.18f },
    264      kPqTransferFunction = { .to_linear = &pq_to_linear,
    265                              .from_linear = &pq_from_linear,
    266                              // https://www.itu.int/pub/R-REP-BT.2408-4-2021
    267                              // page 6 (PDF page 8)
    268                              .mid_tone = 26.f / 10000 },
    269      kHlgTransferFunction = { .to_linear = &hlg_to_linear,
    270                               .from_linear = &hlg_from_linear,
    271                               .mid_tone = 26.f / 1000 };
    272 
    273  switch (tc) {
    274    case AOM_CICP_TC_BT_470_M: return &kGamma22TransferFunction;
    275    case AOM_CICP_TC_BT_470_B_G: return &kGamma28TransferFunction;
    276    case AOM_CICP_TC_SRGB: return &kSRgbTransferFunction;
    277    case AOM_CICP_TC_SMPTE_2084: return &kPqTransferFunction;
    278    case AOM_CICP_TC_HLG: return &kHlgTransferFunction;
    279 
    280    default: fatal("unimplemented transfer function %d", tc);
    281  }
    282 }
    283 
    284 static void generate_photon_noise(const photon_noise_args_t *photon_noise_args,
    285                                  aom_film_grain_t *film_grain) {
    286  // Assumes a daylight-like spectrum.
    287  // https://www.strollswithmydog.com/effective-quantum-efficiency-of-sensor/#:~:text=11%2C260%20photons/um%5E2/lx-s
    288  static const float kPhotonsPerLxSPerUm2 = 11260;
    289 
    290  // Order of magnitude for cameras in the 2010-2020 decade, taking the CFA into
    291  // account.
    292  static const float kEffectiveQuantumEfficiency = 0.20f;
    293 
    294  // Also reasonable values for current cameras. The read noise is typically
    295  // higher than this at low ISO settings but it matters less there.
    296  static const float kPhotoResponseNonUniformity = 0.005f;
    297  static const float kInputReferredReadNoise = 1.5f;
    298 
    299  // Focal plane exposure for a mid-tone (typically a 18% reflectance card), in
    300  // lx·s.
    301  const float mid_tone_exposure = 10.f / photon_noise_args->iso_setting;
    302 
    303  // In microns. Assumes a 35mm sensor (36mm × 24mm).
    304  const float pixel_area_um2 = (36000 * 24000.f) / (photon_noise_args->width *
    305                                                    photon_noise_args->height);
    306 
    307  const float mid_tone_electrons_per_pixel = kEffectiveQuantumEfficiency *
    308                                             kPhotonsPerLxSPerUm2 *
    309                                             mid_tone_exposure * pixel_area_um2;
    310  const float max_electrons_per_pixel =
    311      mid_tone_electrons_per_pixel /
    312      photon_noise_args->transfer_function->mid_tone;
    313 
    314  int i;
    315 
    316  film_grain->num_y_points = 14;
    317  for (i = 0; i < film_grain->num_y_points; ++i) {
    318    float x = i / (film_grain->num_y_points - 1.f);
    319    const float linear = photon_noise_args->transfer_function->to_linear(x);
    320    const float electrons_per_pixel = max_electrons_per_pixel * linear;
    321    // Quadrature sum of the relevant sources of noise, in electrons rms. Photon
    322    // shot noise is sqrt(electrons) so we can skip the square root and the
    323    // squaring.
    324    // https://en.wikipedia.org/wiki/Addition_in_quadrature
    325    // https://doi.org/10.1117/3.725073
    326    const float noise_in_electrons =
    327        sqrtf(kInputReferredReadNoise * kInputReferredReadNoise +
    328              electrons_per_pixel +
    329              (kPhotoResponseNonUniformity * kPhotoResponseNonUniformity *
    330               electrons_per_pixel * electrons_per_pixel));
    331    const float linear_noise = noise_in_electrons / max_electrons_per_pixel;
    332    const float linear_range_start = maxf(0.f, linear - 2 * linear_noise);
    333    const float linear_range_end = minf(1.f, linear + 2 * linear_noise);
    334    const float tf_slope =
    335        (photon_noise_args->transfer_function->from_linear(linear_range_end) -
    336         photon_noise_args->transfer_function->from_linear(
    337             linear_range_start)) /
    338        (linear_range_end - linear_range_start);
    339    float encoded_noise = linear_noise * tf_slope;
    340 
    341    x = roundf(255 * x);
    342    encoded_noise = minf(255.f, roundf(255 * 7.88f * encoded_noise));
    343 
    344    film_grain->scaling_points_y[i][0] = (int)x;
    345    film_grain->scaling_points_y[i][1] = (int)encoded_noise;
    346  }
    347 
    348  film_grain->apply_grain = 1;
    349  film_grain->update_parameters = 1;
    350  film_grain->num_cb_points = 0;
    351  film_grain->num_cr_points = 0;
    352  film_grain->scaling_shift = 8;
    353  film_grain->ar_coeff_lag = 0;
    354  film_grain->ar_coeffs_cb[0] = 0;
    355  film_grain->ar_coeffs_cr[0] = 0;
    356  film_grain->ar_coeff_shift = 6;
    357  film_grain->cb_mult = 0;
    358  film_grain->cb_luma_mult = 0;
    359  film_grain->cb_offset = 0;
    360  film_grain->cr_mult = 0;
    361  film_grain->cr_luma_mult = 0;
    362  film_grain->cr_offset = 0;
    363  film_grain->overlap_flag = 1;
    364  film_grain->random_seed = 7391;
    365  film_grain->chroma_scaling_from_luma = 0;
    366 }
    367 
    368 int main(int argc, char **argv) {
    369  photon_noise_args_t photon_noise_args;
    370  aom_film_grain_table_t film_grain_table;
    371  aom_film_grain_t film_grain;
    372  struct aom_internal_error_info error_info;
    373  memset(&photon_noise_args, 0, sizeof(photon_noise_args));
    374  memset(&film_grain_table, 0, sizeof(film_grain_table));
    375  memset(&film_grain, 0, sizeof(film_grain));
    376  memset(&error_info, 0, sizeof(error_info));
    377 
    378  exec_name = argv[0];
    379  parse_args(argc, argv, &photon_noise_args);
    380 
    381  generate_photon_noise(&photon_noise_args, &film_grain);
    382  aom_film_grain_table_append(&film_grain_table, 0, 9223372036854775807ull,
    383                              &film_grain);
    384  if (aom_film_grain_table_write(&film_grain_table,
    385                                 photon_noise_args.output_filename,
    386                                 &error_info) != AOM_CODEC_OK) {
    387    aom_film_grain_table_free(&film_grain_table);
    388    fprintf(stderr, "Failed to write film grain table");
    389    if (error_info.has_detail) {
    390      fprintf(stderr, ": %s", error_info.detail);
    391    }
    392    fprintf(stderr, "\n");
    393    return EXIT_FAILURE;
    394  }
    395  aom_film_grain_table_free(&film_grain_table);
    396 
    397  return EXIT_SUCCESS;
    398 }