package filmgrain import ( "errors" "fmt" "math" "strings" ) func CreatePhotonNoiseTable(width, height int, iso float64, transferFunction *TransferFunction) ([]byte, error) { if transferFunction == nil { return nil, errors.New("unknown transfer function") } // Assumes a daylight-like spectrum. // https://www.strollswithmydog.com/effective-quantum-efficiency-of-sensor/#:~:text=11%2C260%20photons/um%5E2/lx-s const kPhotonsPerLxSPerUm2 = 11260 // Order of magnitude for cameras in the 2010-2020 decade, taking the CFA into // account. const kEffectiveQuantumEfficiency = 0.20 // Also reasonable values for current cameras. The read noise is typically // higher than this at low ISO settings but it matters less there. const kPhotoResponseNonUniformity = 0.005 const kInputReferredReadNoise = 1.5 // Focal plane exposure for a mid-tone (typically a 18% reflectance card), in // lx·s. midToneExposure := 10 / float64(iso) // In microns. Assumes a 35mm sensor (36mm × 24mm). pixelAreaUm2 := float64((36000 * 24000) / (width * height)) midToneElectronsPerPixel := kEffectiveQuantumEfficiency * kPhotonsPerLxSPerUm2 * midToneExposure * pixelAreaUm2 maxElectronsPerPixel := midToneElectronsPerPixel / transferFunction.MidTone var filmGrain struct { // 8 bit values scalingPointsY [14][2]int numYPoints int // value: 0..14 // 8 bit values scalingPointsCb [10][2]int numCbPoints int // value: 0..10 // 8 bit values scalingPointsCr [10][2]int numCrPoints int // value: 0..10 arCoeffLag int // values: 0..3 // 8 bit values arCoeffsY [24]int arCoeffsCb [25]int arCoeffsCr [25]int } filmGrain.numYPoints = 14 filmGrain.numCbPoints = 0 filmGrain.numCrPoints = 0 filmGrain.arCoeffLag = 0 for i := 0; i < filmGrain.numYPoints; i++ { x := float64(i) / float64(filmGrain.numYPoints-1) linear := transferFunction.ToLinear(x) electronsPerPixel := maxElectronsPerPixel * linear // Quadrature sum of the relevant sources of noise, in electrons rms. Photon // shot noise is math.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 noiseInElectrons := math.Sqrt(kInputReferredReadNoise*kInputReferredReadNoise + electronsPerPixel + (kPhotoResponseNonUniformity * kPhotoResponseNonUniformity * electronsPerPixel * electronsPerPixel)) linearNoise := noiseInElectrons / maxElectronsPerPixel linearRangeStart := max(0., linear-2*linearNoise) linearRangeEnd := min(1., linear+2*linearNoise) tfSlope := (transferFunction.FromLinear(linearRangeEnd) - transferFunction.FromLinear( linearRangeStart)) / (linearRangeEnd - linearRangeStart) encodedNoise := linearNoise * tfSlope x = math.Round(255 * x) encodedNoise = min(255., math.Round(255*7.88*encodedNoise)) filmGrain.scalingPointsY[i][0] = int(x) filmGrain.scalingPointsY[i][1] = int(encodedNoise) } var lines []string lines = append(lines, "filmgrn1") lines = append(lines, fmt.Sprintf("E %d %d %d %d %d", 0 /*start_time*/, math.MaxInt64 /*end_time*/, 1 /*apply_grain*/, 7391 /*seed*/, 1 /*update_parameters*/)) lines = append(lines, fmt.Sprintf("\tp %d %d %d %d %d %d %d %d %d %d %d %d", 0 /*ar_coeff_lag*/, 6 /*ar_coeff_shift*/, 0, /*grain_scale_shift*/ 8 /*scaling_shift*/, 0, /*chroma_scaling_from_luma*/ 1 /*overlap_flag*/, 0 /*cb_mult*/, 0, /*cb_luma_mult*/ 0 /*cb_offset*/, 0 /*cr_mult*/, 0, /*cr_luma_mult*/ 0 /*cr_offset*/)) { line := fmt.Sprintf("\tsY %d ", filmGrain.numYPoints) for i := 0; i < filmGrain.numYPoints; i++ { line += fmt.Sprintf(" %d %d", filmGrain.scalingPointsY[i][0], filmGrain.scalingPointsY[i][1]) } lines = append(lines, line) } { line := fmt.Sprintf("\tsCb %d", filmGrain.numCbPoints) for i := 0; i < filmGrain.numCbPoints; i++ { line += fmt.Sprintf(" %d %d", filmGrain.scalingPointsCb[i][0], filmGrain.scalingPointsCb[i][1]) } lines = append(lines, line) } { line := fmt.Sprintf("\tsCr %d", filmGrain.numCrPoints) for i := 0; i < filmGrain.numCrPoints; i++ { line += fmt.Sprintf(" %d %d", filmGrain.scalingPointsCr[i][0], filmGrain.scalingPointsCr[i][1]) } lines = append(lines, line) } n := 2 * filmGrain.arCoeffLag * (filmGrain.arCoeffLag + 1) { line := "\tcY" for i := 0; i < n; i++ { line += fmt.Sprintf(" %d", filmGrain.arCoeffsY[i]) } lines = append(lines, line) } { line := "\tcCb" for i := 0; i <= n; i++ { line += fmt.Sprintf(" %d", filmGrain.arCoeffsCb[i]) } lines = append(lines, line) } { line := "\tcCr" for i := 0; i <= n; i++ { line += fmt.Sprintf(" %d", filmGrain.arCoeffsCr[i]) } lines = append(lines, line) } return []byte(strings.Join(lines, "\n") + "\n"), nil }