2022-04-20 20:24:55 +00:00
|
|
|
|
use std::fs::File;
|
|
|
|
|
use std::io::{BufWriter, Write};
|
|
|
|
|
use std::path::Path;
|
2022-01-01 21:13:03 +00:00
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
pub struct NoiseGenArgs {
|
|
|
|
|
pub iso_setting: u32,
|
|
|
|
|
pub width: u32,
|
|
|
|
|
pub height: u32,
|
|
|
|
|
pub transfer_function: TransferFunction,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const NUM_Y_POINTS: usize = 14;
|
|
|
|
|
type ScalingPoints = [[u8; 2]; NUM_Y_POINTS];
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::upper_case_acronyms)]
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
pub enum TransferFunction {
|
|
|
|
|
/// For SDR content
|
|
|
|
|
BT1886,
|
|
|
|
|
/// For HDR content
|
|
|
|
|
SMPTE2084,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PQ_M1: f32 = 2610. / 16384.;
|
|
|
|
|
const PQ_M2: f32 = 128. * 2523. / 4096.;
|
|
|
|
|
const PQ_C1: f32 = 3424. / 4096.;
|
|
|
|
|
const PQ_C2: f32 = 32. * 2413. / 4096.;
|
|
|
|
|
const PQ_C3: f32 = 32. * 2392. / 4096.;
|
|
|
|
|
|
|
|
|
|
impl TransferFunction {
|
|
|
|
|
pub fn to_linear(self, x: f32) -> f32 {
|
|
|
|
|
match self {
|
|
|
|
|
TransferFunction::BT1886 => x.powf(2.8),
|
|
|
|
|
TransferFunction::SMPTE2084 => {
|
|
|
|
|
let pq_pow_inv_m2 = x.powf(1. / PQ_M2);
|
2022-01-05 03:48:35 +00:00
|
|
|
|
(0_f32.max(pq_pow_inv_m2 - PQ_C1) / (PQ_C2 - PQ_C3 * pq_pow_inv_m2)).powf(1. / PQ_M1)
|
2022-01-01 21:13:03 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::wrong_self_convention)]
|
|
|
|
|
pub fn from_linear(self, x: f32) -> f32 {
|
|
|
|
|
match self {
|
|
|
|
|
TransferFunction::BT1886 => x.powf(1. / 2.8),
|
|
|
|
|
TransferFunction::SMPTE2084 => {
|
|
|
|
|
let linear_pow_m1 = x.powf(PQ_M1);
|
|
|
|
|
(PQ_C2.mul_add(linear_pow_m1, PQ_C1) / PQ_C3.mul_add(linear_pow_m1, 1.)).powf(PQ_M2)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn mid_tone(self) -> f32 {
|
|
|
|
|
match self {
|
|
|
|
|
TransferFunction::BT1886 => 0.18,
|
|
|
|
|
TransferFunction::SMPTE2084 => 26. / 10000.,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn generate_photon_noise(args: NoiseGenArgs) -> ScalingPoints {
|
|
|
|
|
// Assumes a daylight-like spectrum.
|
|
|
|
|
// https://www.strollswithmydog.com/effective-quantum-efficiency-of-sensor/#:~:text=11%2C260%20photons/um%5E2/lx-s
|
|
|
|
|
const PHOTONS_PER_SQ_MICRON_PER_LUX_SECOND: f32 = 11260.;
|
|
|
|
|
|
|
|
|
|
// Order of magnitude for cameras in the 2010-2020 decade, taking the CFA into account.
|
|
|
|
|
const EFFECTIVE_QUANTUM_EFFICIENCY: f32 = 0.2;
|
|
|
|
|
|
|
|
|
|
// Also reasonable values for current cameras. The read noise is typically
|
|
|
|
|
// higher than this at low ISO settings but it matters less there.
|
|
|
|
|
const PHOTO_RESPONSE_NON_UNIFORMITY: f32 = 0.005;
|
|
|
|
|
const INPUT_REFERRED_READ_NOISE: f32 = 1.5;
|
|
|
|
|
|
|
|
|
|
// Focal plane exposure for a mid-tone (typically a 18% reflectance card), in lx·s.
|
|
|
|
|
let mid_tone_exposure = 10. / args.iso_setting as f32;
|
|
|
|
|
|
|
|
|
|
// Assumes a 35mm sensor (36mm × 24mm).
|
|
|
|
|
const SENSOR_AREA: f32 = 36_000. * 24_000.;
|
|
|
|
|
let pixel_area_microns = SENSOR_AREA / (args.width * args.height) as f32;
|
|
|
|
|
|
|
|
|
|
let mid_tone_electrons_per_pixel = EFFECTIVE_QUANTUM_EFFICIENCY
|
|
|
|
|
* PHOTONS_PER_SQ_MICRON_PER_LUX_SECOND
|
|
|
|
|
* mid_tone_exposure
|
|
|
|
|
* pixel_area_microns;
|
|
|
|
|
let max_electrons_per_pixel = mid_tone_electrons_per_pixel / args.transfer_function.mid_tone();
|
|
|
|
|
|
|
|
|
|
let mut scaling_points = ScalingPoints::default();
|
|
|
|
|
for (i, point) in scaling_points.iter_mut().enumerate() {
|
|
|
|
|
let x = i as f32 / (NUM_Y_POINTS as f32 - 1.);
|
|
|
|
|
let linear = args.transfer_function.to_linear(x);
|
|
|
|
|
let electrons_per_pixel = max_electrons_per_pixel * linear;
|
|
|
|
|
|
|
|
|
|
// Quadrature sum of the relevant sources of noise, in electrons rms. Photon
|
|
|
|
|
// shot noise is sqrt(electrons) so we can skip the square root and the
|
|
|
|
|
// squaring.
|
|
|
|
|
// https://en.wikipedia.org/wiki/Addition_in_quadrature
|
|
|
|
|
// https://doi.org/10.1117/3.725073
|
|
|
|
|
let noise_in_electrons =
|
|
|
|
|
(PHOTO_RESPONSE_NON_UNIFORMITY * PHOTO_RESPONSE_NON_UNIFORMITY * electrons_per_pixel)
|
|
|
|
|
.mul_add(
|
|
|
|
|
electrons_per_pixel,
|
|
|
|
|
INPUT_REFERRED_READ_NOISE.mul_add(INPUT_REFERRED_READ_NOISE, electrons_per_pixel),
|
|
|
|
|
)
|
|
|
|
|
.sqrt();
|
|
|
|
|
let linear_noise = noise_in_electrons / max_electrons_per_pixel;
|
2022-01-05 03:48:35 +00:00
|
|
|
|
let linear_range_start = 0_f32.max(linear - 2. * linear_noise);
|
|
|
|
|
let linear_range_end = 1_f32.min(2_f32.mul_add(linear_noise, linear));
|
2022-01-01 21:13:03 +00:00
|
|
|
|
let tf_slope = (args.transfer_function.from_linear(linear_range_end)
|
|
|
|
|
- args.transfer_function.from_linear(linear_range_start))
|
|
|
|
|
/ (linear_range_end - linear_range_start);
|
|
|
|
|
let encoded_noise = linear_noise * tf_slope;
|
|
|
|
|
|
|
|
|
|
let x = (255. * x).round() as u8;
|
2022-01-05 03:48:35 +00:00
|
|
|
|
let encoded_noise = 255_f32.min((255. * 7.88 * encoded_noise).round()) as u8;
|
2022-01-01 21:13:03 +00:00
|
|
|
|
|
|
|
|
|
point[0] = x;
|
|
|
|
|
point[1] = encoded_noise;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scaling_points
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn create_film_grain_file(
|
|
|
|
|
filename: &Path,
|
|
|
|
|
strength: u8,
|
|
|
|
|
width: u32,
|
|
|
|
|
height: u32,
|
|
|
|
|
transfer: TransferFunction,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
let params = generate_photon_noise(NoiseGenArgs {
|
|
|
|
|
iso_setting: u32::from(strength) * 100,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
transfer_function: transfer,
|
|
|
|
|
});
|
|
|
|
|
let mut file = BufWriter::new(File::create(filename)?);
|
|
|
|
|
write_film_grain_table(params, &mut file)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn write_film_grain_table(
|
|
|
|
|
scaling_points: ScalingPoints,
|
|
|
|
|
file: &mut BufWriter<File>,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
writeln!(file, "filmgrn1")?;
|
|
|
|
|
writeln!(file, "E 0 {} 1 7391 1", i64::MAX)?;
|
|
|
|
|
writeln!(file, "\tp 0 6 0 8 0 1 0 0 0 0 0 0")?;
|
|
|
|
|
write!(file, "\tsY {} ", NUM_Y_POINTS)?;
|
|
|
|
|
for point in &scaling_points {
|
|
|
|
|
write!(file, " {} {}", point[0], point[1])?;
|
|
|
|
|
}
|
|
|
|
|
writeln!(file)?;
|
|
|
|
|
writeln!(file, "\tsCb 0")?;
|
|
|
|
|
writeln!(file, "\tsCr 0")?;
|
|
|
|
|
writeln!(file, "\tcY")?;
|
|
|
|
|
writeln!(file, "\tcCb 0")?;
|
|
|
|
|
writeln!(file, "\tcCr 0")?;
|
|
|
|
|
file.flush()?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|