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 }