Files
QSfera/Server/vendor/github.com/kovidgoyal/imaging/prism/meta/cicp.go
T
Курнат Андрей 2315f25754 Initial QSfera import
2026-06-07 10:20:04 +03:00

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
},
}
}