453 lines
12 KiB
Go
453 lines
12 KiB
Go
package meta
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
|
|
"github.com/kovidgoyal/imaging/prism/meta/icc"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
type CodingIndependentCodePoints struct {
|
|
ColorPrimaries, TransferCharacteristics, MatrixCoefficients, VideoFullRange uint8
|
|
IsSet bool
|
|
}
|
|
|
|
var SRGB = CodingIndependentCodePoints{1, 13, 0, 1, true}
|
|
var DISPLAY_P3 = CodingIndependentCodePoints{12, 13, 0, 1, true}
|
|
|
|
func (c CodingIndependentCodePoints) String() string {
|
|
return fmt.Sprintf("CodingIndependentCodePoints{ColorPrimaries: %d, TransferCharacteristics: %d, MatrixCoefficients: %d, VideoFullRange: %d}", c.ColorPrimaries, c.TransferCharacteristics, c.MatrixCoefficients, c.VideoFullRange)
|
|
}
|
|
|
|
func (c CodingIndependentCodePoints) IsSRGB() bool {
|
|
return c == SRGB
|
|
}
|
|
|
|
func (c CodingIndependentCodePoints) VideoFullRangeIsValid() bool {
|
|
return c.VideoFullRange == 0 || c.VideoFullRange == 1
|
|
}
|
|
|
|
// See https://www.w3.org/TR/png-3/#cICP-chunk for why we do this
|
|
func extend_over_full_range(f func(float64) float64) func(float64) float64 {
|
|
return func(x float64) float64 {
|
|
return math.Copysign(1, x) * f(math.Abs(x))
|
|
}
|
|
}
|
|
|
|
func (src CodingIndependentCodePoints) PipelineTo(dest CodingIndependentCodePoints) *icc.Pipeline {
|
|
if src == dest {
|
|
return nil
|
|
}
|
|
if src.MatrixCoefficients != 0 || dest.MatrixCoefficients != 0 {
|
|
return nil // TODO: Add support for these
|
|
}
|
|
if !src.VideoFullRangeIsValid() || !dest.VideoFullRangeIsValid() {
|
|
return nil
|
|
}
|
|
p := primaries[int(src.ColorPrimaries)]
|
|
if p.Name == "" {
|
|
return nil
|
|
}
|
|
tc := transfer_functions[int(src.TransferCharacteristics)]
|
|
if tc.Name == "" {
|
|
return nil
|
|
}
|
|
to_linear := icc.NewUniformFunctionTransformer(tc.Name, icc.IfElse(src.VideoFullRange == SRGB.VideoFullRange, tc.EOTF, extend_over_full_range(tc.EOTF)))
|
|
if tc.Name == "Identity" {
|
|
to_linear = nil
|
|
}
|
|
linear_to_xyz := p.CalculateRGBtoXYZMatrix()
|
|
p = primaries[int(dest.ColorPrimaries)]
|
|
if p.Name == "" {
|
|
return nil
|
|
}
|
|
tc = transfer_functions[int(dest.TransferCharacteristics)]
|
|
if tc.Name == "" {
|
|
return nil
|
|
}
|
|
xyz_to_linear := p.CalculateRGBtoXYZMatrix()
|
|
xyz_to_linear, err := xyz_to_linear.Inverted()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
f := icc.IfElse(dest.VideoFullRange == SRGB.VideoFullRange, tc.OETF, extend_over_full_range(tc.OETF))
|
|
from_linear := icc.NewUniformFunctionTransformer(tc.Name, func(x float64) float64 {
|
|
// TODO: Gamut mapping for white point of dest, re-use code from colorconv
|
|
return max(0, min(f(x), 1))
|
|
})
|
|
if tc.Name == "Identity" {
|
|
from_linear = nil
|
|
}
|
|
ans := &icc.Pipeline{}
|
|
ans.Append(to_linear, &linear_to_xyz, &xyz_to_linear, from_linear)
|
|
ans.Finalize(true)
|
|
return ans
|
|
}
|
|
|
|
func (c CodingIndependentCodePoints) PipelineToSRGB() *icc.Pipeline {
|
|
return c.PipelineTo(SRGB)
|
|
}
|
|
|
|
// XY holds CIE xy chromaticity coordinates.
|
|
type XY struct {
|
|
X, Y float64
|
|
}
|
|
|
|
// ColorSpace defines the primaries and white point of a color space.
|
|
type Primaries struct {
|
|
Name string
|
|
Red XY
|
|
Green XY
|
|
Blue XY
|
|
White XY
|
|
}
|
|
|
|
// xyToXYZ converts xy chromaticity to XYZ coordinates, assuming Y=1.
|
|
func xyToXYZ(p XY) [3]float64 {
|
|
if p.Y == 0 {
|
|
return [3]float64{0, 0, 0}
|
|
}
|
|
return [3]float64{
|
|
p.X / p.Y,
|
|
1.0,
|
|
(1.0 - p.X - p.Y) / p.Y,
|
|
}
|
|
}
|
|
|
|
// CalculateRGBtoXYZMatrix computes the matrix to convert from a linear RGB color space to CIE XYZ.
|
|
func (cs *Primaries) CalculateRGBtoXYZMatrix() icc.Matrix3 {
|
|
// Convert primaries to XYZ space (normalized to Y=1)
|
|
r := xyToXYZ(cs.Red)
|
|
g := xyToXYZ(cs.Green)
|
|
b := xyToXYZ(cs.Blue)
|
|
|
|
// Form the matrix of primaries
|
|
M := icc.Matrix3{
|
|
{r[0], g[0], b[0]},
|
|
{r[1], g[1], b[1]},
|
|
{r[2], g[2], b[2]},
|
|
}
|
|
|
|
// Calculate the scaling factors (S_r, S_g, S_b)
|
|
invM, err := M.Inverted()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
whiteXYZ := xyToXYZ(cs.White)
|
|
|
|
s_r := invM[0][0]*whiteXYZ[0] + invM[0][1]*whiteXYZ[1] + invM[0][2]*whiteXYZ[2]
|
|
s_g := invM[1][0]*whiteXYZ[0] + invM[1][1]*whiteXYZ[1] + invM[1][2]*whiteXYZ[2]
|
|
s_b := invM[2][0]*whiteXYZ[0] + invM[2][1]*whiteXYZ[1] + invM[2][2]*whiteXYZ[2]
|
|
|
|
// Scale the primaries matrix to get the final conversion matrix
|
|
finalMatrix := icc.Matrix3{
|
|
{M[0][0] * s_r, M[0][1] * s_g, M[0][2] * s_b},
|
|
{M[1][0] * s_r, M[1][1] * s_g, M[1][2] * s_b},
|
|
{M[2][0] * s_r, M[2][1] * s_g, M[2][2] * s_b},
|
|
}
|
|
return finalMatrix
|
|
}
|
|
|
|
type WellKnownPrimaries int
|
|
|
|
// These come from ITU-T H.273 spec
|
|
var primaries = map[int]Primaries{
|
|
1: {
|
|
Name: "sRGB",
|
|
Green: XY{X: 0.30, Y: 0.60},
|
|
Blue: XY{X: 0.15, Y: 0.06},
|
|
Red: XY{X: 0.64, Y: 0.33},
|
|
White: XY{X: 0.3127, Y: 0.3290}, // D65
|
|
},
|
|
4: {
|
|
Name: "BT-470M",
|
|
Green: XY{X: 0.21, Y: 0.71},
|
|
Blue: XY{X: 0.14, Y: 0.08},
|
|
Red: XY{X: 0.67, Y: 0.33},
|
|
White: XY{X: 0.310, Y: 0.316},
|
|
},
|
|
5: {
|
|
Name: "BT-470B",
|
|
Green: XY{X: 0.29, Y: 0.69},
|
|
Blue: XY{X: 0.15, Y: 0.06},
|
|
Red: XY{X: 0.64, Y: 0.33},
|
|
White: XY{X: 0.310, Y: 0.316},
|
|
},
|
|
6: {
|
|
Name: "BT-601",
|
|
Green: XY{0.310, 0.595},
|
|
Blue: XY{0.155, 0.070},
|
|
Red: XY{0.630, 0.340},
|
|
White: XY{0.3127, 0.3290},
|
|
},
|
|
7: {
|
|
Name: "BT-601",
|
|
Green: XY{0.310, 0.595},
|
|
Blue: XY{0.155, 0.070},
|
|
Red: XY{0.630, 0.340},
|
|
White: XY{0.3127, 0.3290},
|
|
},
|
|
8: {
|
|
Name: "Generic film",
|
|
Green: XY{0.243, 0.692},
|
|
Blue: XY{0.145, 0.049},
|
|
Red: XY{0.681, 0.319},
|
|
White: XY{0.310, 0.316},
|
|
},
|
|
9: {
|
|
Name: "BT-2020",
|
|
Green: XY{0.170, 0.797},
|
|
Blue: XY{0.131, 0.046},
|
|
Red: XY{0.708, 0.292},
|
|
White: XY{0.3127, 0.3290},
|
|
},
|
|
10: { // 10
|
|
Name: "SMPTE ST 428-1",
|
|
Green: XY{0.0, 1.0},
|
|
Blue: XY{0.0, 0.0},
|
|
Red: XY{1.0, 0.0},
|
|
White: XY{1 / 3., 1 / 3.},
|
|
},
|
|
11: { // 11
|
|
Name: "DCI-P3",
|
|
Green: XY{0.265, 0.690},
|
|
Blue: XY{0.150, 0.060},
|
|
Red: XY{0.680, 0.320},
|
|
White: XY{0.314, 0.351}, // DCI White
|
|
},
|
|
12: { // 12
|
|
Name: "Diplay P3",
|
|
Green: XY{0.265, 0.690},
|
|
Blue: XY{0.150, 0.060},
|
|
Red: XY{0.680, 0.320},
|
|
White: XY{0.3127, 0.3290}, // D65
|
|
},
|
|
22: { // 22
|
|
Name: "Unnamed",
|
|
Green: XY{0.295, 0.605},
|
|
Blue: XY{0.155, 0.077},
|
|
Red: XY{0.630, 0.340},
|
|
White: XY{0.3127, 0.3290}, // D65
|
|
},
|
|
}
|
|
|
|
// TransferFunction defines an Opto-Electronic Transfer Function (OETF)
|
|
// and its inverse Electro-Optical Transfer Function (EOTF).
|
|
type TransferFunction struct {
|
|
ID int
|
|
Name string
|
|
OETF func(float64) float64 // To non-linear
|
|
EOTF func(float64) float64 // To linear
|
|
}
|
|
|
|
// Constants from various specifications used in the transfer functions.
|
|
const (
|
|
// BT.709, BT.2020, BT.601
|
|
alpha709 = 1.099
|
|
beta709 = 0.018
|
|
gamma709 = 0.45
|
|
delta709 = 4.5
|
|
|
|
// SMPTE ST 240M
|
|
alpha240M = 1.1115
|
|
beta240M = 0.0228
|
|
gamma240M = 0.45
|
|
delta240M = 4.0
|
|
|
|
// SMPTE ST 428-1
|
|
gamma428 = 1.0 / 2.6
|
|
|
|
// PQ (Perceptual Quantizer) - SMPTE ST 2084
|
|
m1PQ = 2610.0 / 16384.0 // (2610 / 4096) * (1/4)
|
|
m2PQ = 2523.0 / 32.0 // (2523 / 4096) * 128
|
|
c1PQ = 3424.0 / 4096.0
|
|
c2PQ = 2413.0 / 4096.0 * 32.0
|
|
c3PQ = 2392.0 / 4096.0 * 32.0
|
|
|
|
// HLG (Hybrid Log-Gamma) - ARIB STD-B67
|
|
aHLG = 0.17883277
|
|
bHLG = 1.0 - 4.0*aHLG // 0.28466892
|
|
cHLG = 0.55991073 // 0.5 - aHLG*math.Log(4.0*aHLG)
|
|
)
|
|
|
|
// holds all the H.273 transfer characteristics.
|
|
var transfer_functions = make(map[int]TransferFunction)
|
|
|
|
func init() {
|
|
tf1 := TransferFunction{
|
|
ID: 1, Name: "BT.709",
|
|
OETF: func(L float64) float64 {
|
|
if L < beta709 {
|
|
return delta709 * L
|
|
}
|
|
return alpha709*math.Pow(L, gamma709) - (alpha709 - 1)
|
|
},
|
|
EOTF: func(V float64) float64 {
|
|
if V < delta709*beta709 {
|
|
return V / delta709
|
|
}
|
|
return math.Pow((V+(alpha709-1))/alpha709, 1.0/gamma709)
|
|
},
|
|
}
|
|
transfer_functions[1] = tf1
|
|
transfer_functions[6] = tf1 // BT.601, BT.2020 share this with BT.709
|
|
transfer_functions[14] = tf1 // BT.2020 10-bit
|
|
transfer_functions[15] = tf1 // BT.2020 12-bit
|
|
|
|
// 2: Identity
|
|
transfer_functions[2] = TransferFunction{
|
|
ID: 2, Name: "Identity",
|
|
OETF: func(v float64) float64 { return v },
|
|
EOTF: func(v float64) float64 { return v },
|
|
}
|
|
transfer_functions[8] = transfer_functions[2]
|
|
|
|
// 4: Gamma 2.2
|
|
tf4 := TransferFunction{
|
|
ID: 4, Name: "Gamma 2.2",
|
|
OETF: func(L float64) float64 { return math.Pow(L, 1.0/2.2) },
|
|
EOTF: func(V float64) float64 { return math.Pow(V, 2.2) },
|
|
}
|
|
transfer_functions[4] = tf4
|
|
transfer_functions[5] = tf4 // BT.470BG also uses Gamma 2.2 approx.
|
|
|
|
// 5: Gamma 2.8
|
|
transfer_functions[5] = TransferFunction{
|
|
ID: 5, Name: "Gamma 2.8",
|
|
OETF: func(L float64) float64 { return math.Pow(L, 1.0/2.8) },
|
|
EOTF: func(V float64) float64 { return math.Pow(V, 2.8) },
|
|
}
|
|
|
|
// 7: SMPTE 240M
|
|
tf7 := TransferFunction{
|
|
ID: 7, Name: "SMPTE 240M",
|
|
OETF: func(L float64) float64 {
|
|
if L < beta240M {
|
|
return delta240M * L
|
|
}
|
|
return alpha240M*math.Pow(L, gamma240M) - (alpha240M - 1)
|
|
},
|
|
EOTF: func(V float64) float64 {
|
|
if V < delta240M*beta240M {
|
|
return V / delta240M
|
|
}
|
|
return math.Pow((V+(alpha240M-1))/alpha240M, 1.0/gamma240M)
|
|
},
|
|
}
|
|
transfer_functions[7] = tf7
|
|
// 9: Logarithmic (100:1)
|
|
transfer_functions[9] = TransferFunction{
|
|
ID: 9, Name: "Logarithmic (100:1)",
|
|
OETF: func(L float64) float64 {
|
|
return 1.0 - math.Log10(1.0-L*(1.0-math.Pow(10.0, -2.0)))/2.0
|
|
},
|
|
EOTF: func(V float64) float64 {
|
|
return (1.0 - math.Pow(10.0, -2.0*V)) / (1.0 - math.Pow(10.0, -2.0))
|
|
},
|
|
}
|
|
|
|
// 10: Logarithmic (100 * sqrt(10):1)
|
|
transfer_functions[10] = TransferFunction{
|
|
ID: 10, Name: "Logarithmic (100*sqrt(10):1)",
|
|
OETF: func(L float64) float64 {
|
|
return 1.0 - math.Log10(1.0-L*(1.0-math.Pow(10.0, -2.5)))/2.5
|
|
},
|
|
EOTF: func(V float64) float64 {
|
|
return (1.0 - math.Pow(10.0, -2.5*V)) / (1.0 - math.Pow(10.0, -2.5))
|
|
},
|
|
}
|
|
|
|
// 11: IEC 61966-2-4
|
|
transfer_functions[11] = TransferFunction{
|
|
ID: 11, Name: "IEC 61966-2-4",
|
|
OETF: func(L float64) float64 {
|
|
if L < -beta709 {
|
|
return -delta709 * -L
|
|
}
|
|
if L > beta709 {
|
|
return alpha709*math.Pow(L, gamma709) - (alpha709 - 1)
|
|
}
|
|
return delta709 * L
|
|
},
|
|
EOTF: func(V float64) float64 {
|
|
if V < -delta709*beta709 {
|
|
return -math.Pow((-V+(alpha709-1))/alpha709, 1.0/gamma709)
|
|
}
|
|
if V > delta709*beta709 {
|
|
return math.Pow((V+(alpha709-1))/alpha709, 1.0/gamma709)
|
|
}
|
|
return V / delta709
|
|
},
|
|
}
|
|
|
|
// 12: BT.1361 extended gamut
|
|
tf12 := tf1 // It's based on BT.709
|
|
tf12.ID = 12
|
|
tf12.Name = "BT.1361"
|
|
transfer_functions[12] = tf12
|
|
|
|
// 13: sRGB/IEC 61966-2-1
|
|
transfer_functions[13] = TransferFunction{
|
|
ID: 13, Name: "sRGB",
|
|
OETF: func(L float64) float64 {
|
|
if L <= 0.0031308 {
|
|
return 12.92 * L
|
|
}
|
|
return 1.055*math.Pow(L, 1.0/2.4) - 0.055
|
|
},
|
|
EOTF: func(V float64) float64 {
|
|
if V <= 0.04045 {
|
|
return V / 12.92
|
|
}
|
|
return math.Pow((V+0.055)/1.055, 2.4)
|
|
},
|
|
}
|
|
// 16: SMPTE ST 2084 (PQ)
|
|
transfer_functions[16] = TransferFunction{
|
|
ID: 16, Name: "SMPTE ST 2084 (PQ)",
|
|
OETF: func(L float64) float64 { // EOTF^-1, L is normalized to 10000 cd/m^2
|
|
Lp := math.Pow(L, m1PQ)
|
|
return math.Pow((c1PQ+c2PQ*Lp)/(1.0+c3PQ*Lp), m2PQ)
|
|
},
|
|
EOTF: func(V float64) float64 { // V is non-linear signal
|
|
Vp := math.Pow(V, 1.0/m2PQ)
|
|
num := math.Max(Vp-c1PQ, 0.0)
|
|
den := math.Max(c2PQ-c3PQ*Vp, 1e-6) // Avoid division by zero
|
|
return math.Pow(num/den, 1.0/m1PQ)
|
|
},
|
|
}
|
|
|
|
// 17: SMPTE ST 428-1
|
|
transfer_functions[17] = TransferFunction{
|
|
ID: 17, Name: "SMPTE ST 428-1",
|
|
OETF: func(L float64) float64 { // OOTF^-1, from linear scene light to D-cinema
|
|
// Input L is assumed to be scene linear (48 cd/m^2 peak)
|
|
// The spec normalizes by 52.37
|
|
return math.Pow((L*48.0)/52.37, gamma428)
|
|
},
|
|
EOTF: func(V float64) float64 { // OOTF
|
|
// Output is linear light, normalized to 1.0 for peak white (48 cd/m^2)
|
|
return (52.37 / 48.0) * math.Pow(V, 1.0/gamma428)
|
|
},
|
|
}
|
|
|
|
// 18: ARIB STD-B67 (HLG)
|
|
transfer_functions[18] = TransferFunction{
|
|
ID: 18, Name: "ARIB STD-B67 (HLG)",
|
|
OETF: func(L float64) float64 { // L is scene linear light, display-referred
|
|
if L <= 1.0/12.0 {
|
|
return math.Sqrt(3.0 * L)
|
|
}
|
|
return aHLG*math.Log(12.0*L-bHLG) + cHLG
|
|
},
|
|
EOTF: func(V float64) float64 { // V is the non-linear signal
|
|
if V <= 0.5 {
|
|
return (V * V) / 3.0
|
|
}
|
|
return (math.Exp((V-cHLG)/aHLG) + bHLG) / 12.0
|
|
},
|
|
}
|
|
}
|