use std::{
fs::File,
io::{BufWriter, Write},
path::Path,
};
use arrayvec::ArrayVec;
use crate::{GrainTableSegment, ScalingPoints, DEFAULT_GRAIN_SEED, NUM_Y_POINTS};
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.;
const BT1886_WHITEPOINT: f32 = 203.;
const BT1886_BLACKPOINT: f32 = 0.1;
const BT1886_GAMMA: f32 = 2.4;
#[inline(always)]
fn bt1886_inv_whitepoint() -> f32 {
BT1886_WHITEPOINT.powf(1.0 / BT1886_GAMMA)
}
#[inline(always)]
fn bt1886_inv_blackpoint() -> f32 {
BT1886_BLACKPOINT.powf(1.0 / BT1886_GAMMA)
}
#[inline(always)]
fn bt1886_alpha() -> f32 {
(bt1886_inv_whitepoint() - bt1886_inv_blackpoint()).powf(BT1886_GAMMA)
}
#[inline(always)]
fn bt1886_beta() -> f32 {
bt1886_inv_blackpoint() / (bt1886_inv_whitepoint() - bt1886_inv_blackpoint())
}
#[derive(Debug, Clone, Copy)]
pub struct NoiseGenArgs {
pub iso_setting: u32,
pub width: u32,
pub height: u32,
pub transfer_function: TransferFunction,
pub chroma_grain: bool,
pub random_seed: Option<u16>,
}
#[must_use]
pub fn generate_photon_noise_params(
start_time: u64,
end_time: u64,
args: NoiseGenArgs,
) -> GrainTableSegment {
GrainTableSegment {
start_time,
end_time,
scaling_points_y: generate_luma_noise_points(args),
scaling_points_cb: ArrayVec::new(),
scaling_points_cr: ArrayVec::new(),
scaling_shift: 8,
ar_coeff_lag: 0,
ar_coeffs_y: ArrayVec::new(),
ar_coeffs_cb: ArrayVec::try_from([0].as_slice())
.expect("Cannot fail creation from const array"),
ar_coeffs_cr: ArrayVec::try_from([0].as_slice())
.expect("Cannot fail creation from const array"),
ar_coeff_shift: 6,
cb_mult: 0,
cb_luma_mult: 0,
cb_offset: 0,
cr_mult: 0,
cr_luma_mult: 0,
cr_offset: 0,
overlap_flag: true,
chroma_scaling_from_luma: args.chroma_grain,
grain_scale_shift: 0,
random_seed: args.random_seed.unwrap_or(DEFAULT_GRAIN_SEED),
}
}
#[must_use]
#[cfg(feature = "unstable")]
pub fn generate_film_grain_params(
start_time: u64,
end_time: u64,
args: NoiseGenArgs,
) -> GrainTableSegment {
todo!("SCIENCE");
}
pub fn write_grain_table<P: AsRef<Path>>(
filename: P,
params: &[GrainTableSegment],
) -> anyhow::Result<()> {
let mut file = BufWriter::new(File::create(filename)?);
writeln!(&mut file, "filmgrn1")?;
for segment in params {
write_film_grain_segment(segment, &mut file)?;
}
file.flush()?;
Ok(())
}
fn write_film_grain_segment(
params: &GrainTableSegment,
output: &mut BufWriter<File>,
) -> anyhow::Result<()> {
writeln!(
output,
"E {} {} 1 {} 1",
params.start_time, params.end_time, params.random_seed,
)?;
writeln!(
output,
"\tp {} {} {} {} {} {} {} {} {} {} {} {}",
params.ar_coeff_lag,
params.ar_coeff_shift,
params.grain_scale_shift,
params.scaling_shift,
u8::from(params.chroma_scaling_from_luma),
u8::from(params.overlap_flag),
params.cb_mult,
params.cb_luma_mult,
params.cb_offset,
params.cr_mult,
params.cr_luma_mult,
params.cr_offset
)?;
write!(output, "\tsY {} ", params.scaling_points_y.len())?;
for point in ¶ms.scaling_points_y {
write!(output, " {} {}", point[0], point[1])?;
}
writeln!(output)?;
write!(output, "\tsCb {}", params.scaling_points_cb.len())?;
for point in ¶ms.scaling_points_cb {
write!(output, " {} {}", point[0], point[1])?;
}
writeln!(output)?;
write!(output, "\tsCr {}", params.scaling_points_cr.len())?;
for point in ¶ms.scaling_points_cr {
write!(output, " {} {}", point[0], point[1])?;
}
writeln!(output)?;
write!(output, "\tcY")?;
for coeff in ¶ms.ar_coeffs_y {
write!(output, " {}", *coeff)?;
}
writeln!(output)?;
write!(output, "\tcCb")?;
for coeff in ¶ms.ar_coeffs_cb {
write!(output, " {}", *coeff)?;
}
writeln!(output)?;
write!(output, "\tcCr")?;
for coeff in ¶ms.ar_coeffs_cr {
write!(output, " {}", *coeff)?;
}
writeln!(output)?;
Ok(())
}
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransferFunction {
BT1886,
SMPTE2084,
}
impl TransferFunction {
#[must_use]
pub fn to_linear(self, x: f32) -> f32 {
match self {
TransferFunction::BT1886 => {
let luma = bt1886_alpha() * (x + bt1886_beta()).powf(BT1886_GAMMA);
luma / BT1886_WHITEPOINT
}
TransferFunction::SMPTE2084 => {
let pq_pow_inv_m2 = x.powf(1. / PQ_M2);
(0_f32.max(pq_pow_inv_m2 - PQ_C1) / (PQ_C2 - PQ_C3 * pq_pow_inv_m2))
.powf(1. / PQ_M1)
}
}
}
#[allow(clippy::wrong_self_convention)]
#[must_use]
pub fn from_linear(self, x: f32) -> f32 {
match self {
TransferFunction::BT1886 => {
let luma = x * BT1886_WHITEPOINT;
(luma / bt1886_alpha()).powf(1.0 / BT1886_GAMMA) - bt1886_beta()
}
TransferFunction::SMPTE2084 => {
if x < f32::EPSILON {
return 0.0;
}
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)
}
}
}
#[inline(always)]
#[must_use]
pub fn mid_tone(self) -> f32 {
self.to_linear(0.5)
}
}
fn generate_luma_noise_points(args: NoiseGenArgs) -> ScalingPoints {
const PHOTONS_PER_SQ_MICRON_PER_LUX_SECOND: f32 = 11260.;
const EFFECTIVE_QUANTUM_EFFICIENCY: f32 = 0.2;
const PHOTO_RESPONSE_NON_UNIFORMITY: f32 = 0.005;
const INPUT_REFERRED_READ_NOISE: f32 = 1.5;
const SENSOR_AREA: f32 = 36_000. * 24_000.;
let mid_tone_exposure = 10. / args.iso_setting as f32;
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 in 0..NUM_Y_POINTS {
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;
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;
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));
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;
let encoded_noise = 255_f32.min((255. * 7.88 * encoded_noise).round()) as u8;
scaling_points.push([x, encoded_noise]);
}
scaling_points
}
#[cfg(test)]
mod tests {
use quickcheck::TestResult;
use quickcheck_macros::quickcheck;
use super::*;
#[quickcheck]
fn bt1886_to_linear_within_range(x: f32) -> TestResult {
if !(0.0..=1.0).contains(&x) || x.is_nan() {
return TestResult::discard();
}
let tx = TransferFunction::BT1886;
let res = tx.to_linear(x);
TestResult::from_bool((0.0..=1.0).contains(&res))
}
#[quickcheck]
fn bt1886_to_linear_reverts_correctly(x: f32) -> TestResult {
if !(0.0..=1.0).contains(&x) || x.is_nan() {
return TestResult::discard();
}
let tx = TransferFunction::BT1886;
let res = tx.to_linear(x);
let res = tx.from_linear(res);
TestResult::from_bool((x - res).abs() < f32::EPSILON)
}
#[quickcheck]
fn smpte2084_to_linear_within_range(x: f32) -> TestResult {
if !(0.0..=1.0).contains(&x) || x.is_nan() {
return TestResult::discard();
}
let tx = TransferFunction::SMPTE2084;
let res = tx.to_linear(x);
TestResult::from_bool((0.0..=1.0).contains(&res))
}
#[quickcheck]
fn smpte2084_to_linear_reverts_correctly(x: f32) -> TestResult {
if !(0.0..=1.0).contains(&x) || x.is_nan() {
return TestResult::discard();
}
let tx = TransferFunction::SMPTE2084;
let res = tx.to_linear(x);
let res = tx.from_linear(res);
TestResult::from_bool((x - res).abs() < f32::EPSILON)
}
}