Ignite/utilities/filmgrain/transfer.go

142 lines
3.9 KiB
Go
Raw Normal View History

package filmgrain
import (
"math"
"strings"
)
//TODO: transfer function type?
func GetTransferFunction(kind string) *TransferFunction {
switch strings.ToLower(kind) {
case "bt470m":
//gamma22
return &TransferFunction{
ToLinear: func(in float64) (linear float64) {
return math.Pow(in, 2.2)
},
FromLinear: func(linear float64) (out float64) {
return math.Pow(linear, 1/2.2)
},
MidTone: 0.18,
}
case "bt470bg":
//gamma28
return &TransferFunction{
ToLinear: func(in float64) (linear float64) {
return math.Pow(in, 2.8)
},
FromLinear: func(linear float64) (out float64) {
return math.Pow(linear, 1/2.8)
},
MidTone: 0.18,
}
case "bt601", "bt709", "bt2020":
//TODO: are bt601 and bt709 the same?
//bt2020. same as bt709, just defined more precise. Can use the same for all
const beta = 0.018053968510807
const alpha = 1 + 5.5*beta
return &TransferFunction{
ToLinear: func(in float64) (linear float64) {
if in < 0.081 {
return in / 4.5
} else {
return math.Pow((in+(alpha-1))/alpha, 1/0.45)
}
},
FromLinear: func(linear float64) (out float64) {
if linear < beta {
return 4.5 * linear
} else {
return alpha*math.Pow(linear, 0.45) - (alpha - 1)
}
},
//TODO: check this for bt2020?
MidTone: 0.18,
}
case "srgb":
//sRGB
return &TransferFunction{
ToLinear: func(in float64) (linear float64) {
if in <= 0.04045 {
return in / 12.92
} else {
return math.Pow((in+0.055)/1.055, 2.4)
}
},
FromLinear: func(linear float64) (out float64) {
if linear <= 0.0031308 {
return 12.92 * linear
} else {
return 1.055*math.Pow(linear, 1/2.4) - 0.055
}
},
MidTone: 0.18,
}
case "smpte2084", "pq":
//pq
const PqM1 = 2610. / 16384
const PqM2 = 128 * 2523. / 4096
const PqC1 = 3424. / 4096
const PqC2 = 32 * 2413. / 4096
const PqC3 = 32 * 2392. / 4096
return &TransferFunction{
ToLinear: func(in float64) (linear float64) {
pq_pow_inv_m2 := math.Pow(in, 1./PqM2)
return math.Pow(max(0, pq_pow_inv_m2-PqC1)/(PqC2-PqC3*pq_pow_inv_m2),
1./PqM1)
},
FromLinear: func(linear float64) (out float64) {
linear_pow_m1 := math.Pow(linear, PqM1)
return math.Pow((PqC1+PqC2*linear_pow_m1)/(1+PqC3*linear_pow_m1),
PqM2)
},
MidTone: 26. / 1000,
}
case "hlg":
//hlg
// Note: it is perhaps debatable whether “linear” for HLG should be scene light
// or display light. Here, it is implemented in terms of display light assuming
// a nominal peak display luminance of 1000 cd/m², hence the system γ of 1.2. To
// make it scene light instead, the OOTF (math.Pow(x, 1.2)) and its inverse should
// be removed from the functions below, and the TransferFunction.MidTone should be replaced
// with math.Pow(26. / 1000, 1 / 1.2).
const HlgA = 0.17883277
const HlgB = 0.28466892
const HlgC = 0.55991073
return &TransferFunction{
ToLinear: func(in float64) (linear float64) {
// EOTF = OOTF ∘ OETF⁻¹
if in <= 0.5 {
linear = in * in / 3
} else {
linear = (math.Exp((in-HlgC)/HlgA) + HlgB) / 12
}
return math.Pow(linear, 1.2)
},
FromLinear: func(linear float64) (out float64) {
// EOTF⁻¹ = OETF ∘ OOTF⁻¹
linear = math.Pow(linear, 1./1.2)
if linear <= (1. / 12) {
return math.Sqrt(3 * linear)
} else {
return HlgA*math.Log(12*linear-HlgB) + HlgC
}
},
MidTone: 26. / 1000,
}
}
return nil
}
type TransferFunction struct {
ToLinear func(in float64) (linear float64)
FromLinear func(linear float64) (out float64)
// MidTone In linear output light. This would typically be 0.18 for SDR (this matches
// the definition of Standard Output Sensitivity from ISO 12232:2019), but in
// HDR, we certainly do not want to consider 18% of the maximum output a
// “mid-tone”, as it would be e.g. 1800 cd/m² for SMPTE ST 2084 (PQ).
MidTone float64
}