161 lines
4.7 KiB
Go
161 lines
4.7 KiB
Go
|
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
|
|||
|
}
|