-
Notifications
You must be signed in to change notification settings - Fork 1
/
color-profile-general.go
225 lines (184 loc) · 8.24 KB
/
color-profile-general.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Copyright (c) 2021-2022 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package emission
import (
"fmt"
)
// ColorProfileGeneral is a general implementation of a ColorProfile.
// It supports:
//
// - Up to 3 primary colored emitters.
// - Up to 3 white emitters, so a total of 6 emitters.
// - Custom transfer functions.
// - Custom output limiter functions.
//
// You need to call the Init() or MustInit() method before this profile can be used.
type ColorProfileGeneral struct {
// WhitePointColor is the brightest color that the module can output.
// Usually it's the combination of all white emitters.
// Or of all primary emitters, if there are no white ones, or if the whites are are less bright.
WhitePointColor CIE1931XYZAbs
// The XYZ values of the primary channels that span the gamut for this module.
// All colors that increase the gamut have to go into here.
// This usually describes the 3 primary colors.
PrimaryColors TransformationLinDCSToXYZ
// Precalculated inverse of the above.
invPrimaryColors TransformationXYZToLinDCS
// The XYZ values of channels used to increase the CRI and light output of a lamp.
// This is achieved by maximizing these channels in a way that doesn't change the color or luminance of the resulting color.
// Normally these are the white LEDs which span a color gamut that lies inside that of the PrimaryColors.
WhiteColors TransformationLinDCSToXYZ
// Precalculated inverse of the above.
invWhiteColors TransformationXYZToLinDCS
// Limit the output in some form.
OutputLimiter OutputLimiter
// Transfer function to convert from a linear device color space into a non linear device color space, and vice versa.
// Set to nil if your DCS is linear.
TransferFunc TransferFunction
// Disables high CRI and high luminance optimization by disabling white emitters.
noWhiteOptimization bool
// Variant of this color profile without high CRI white light emitters.
noWhiteOptimizationColorProfile ColorProfile
}
// Check if it implements ColorProfile.
var _ ColorProfile = &ColorProfileGeneral{}
// Init precalculates some values.
func (e *ColorProfileGeneral) Init() error {
var err error
if e.invPrimaryColors, err = e.PrimaryColors.Inverted(); err != nil {
return fmt.Errorf("failed to invert primary color matrix: %w", err)
}
if e.invWhiteColors, err = e.WhiteColors.Inverted(); err != nil {
return fmt.Errorf("failed to invert white color matrix: %w", err)
}
e.noWhiteOptimizationColorProfile = &ColorProfileGeneral{
WhitePointColor: e.WhitePointColor,
PrimaryColors: e.PrimaryColors,
invPrimaryColors: e.invPrimaryColors,
WhiteColors: e.WhiteColors,
invWhiteColors: e.invWhiteColors,
OutputLimiter: e.OutputLimiter,
TransferFunc: e.TransferFunc,
noWhiteOptimizationColorProfile: nil, // Can't easily put a self reference in here, but the getter method will take care of this edge case.
noWhiteOptimization: true,
}
return nil
}
// MustInit is the same as Init(), but panics on any error.
// As a small help, this returns the color profile itself.
func (e *ColorProfileGeneral) MustInit() *ColorProfileGeneral {
if err := e.Init(); err != nil {
panic(fmt.Sprintf("Failed to init module profile %v: %v", e, err))
}
return e
}
// Channels returns the dimensionality of the device color space.
func (e *ColorProfileGeneral) Channels() int {
return len(e.PrimaryColors) + len(e.WhiteColors)
}
// WhitePoint returns the white point as a CIE 1931 XYZ color.
// This is also the brightest color a module can output.
func (e *ColorProfileGeneral) WhitePoint() CIE1931XYZAbs {
return e.WhitePointColor
}
// ChannelPoints returns a list of channel colors.
// Depending on the module type, this could be the colors for:
//
// - Single white emitter.
// - RGB emitters.
// - RGB + white emitters.
// - RGB + cold white + warm white emitters.
func (e *ColorProfileGeneral) ChannelPoints() []CIE1931XYZAbs {
return e.FullTransformation()
}
// FullTransformation returns a transformation (matrix) that contains all channels (A list of all colors).
func (e *ColorProfileGeneral) FullTransformation() TransformationLinDCSToXYZ {
result := make(TransformationLinDCSToXYZ, 0, e.Channels())
return append(append(result, e.PrimaryColors...), e.WhiteColors...)
}
// XYZToDCS takes a color and returns a vector in the device color space that reproduces the given color as closely as possible.
//
// Short: XYZ --> device color space.
func (e *ColorProfileGeneral) XYZToDCS(color CIE1931XYZAbs) DCSVector {
// Determine the DCS values of the primary channels.
primaryV := e.invPrimaryColors.Multiplied(color)
// Optimize for high CRI and high luminance output, if wanted.
whiteV := make(LinDCSVector, e.invWhiteColors.DCSChannels())
if !e.noWhiteOptimization {
// Determine the closest possible DCS values of the white channels.
whiteV = e.invWhiteColors.Multiplied(color).ClampedToPositive()
// Get the color of whiteValues
whiteColor, err := e.WhiteColors.Multiplied(whiteV)
if err != nil {
panic(err) // Shouldn't happen.
}
// Get the proportions of the primary colors that represent whiteColor.
// Can contain negative values if the white is outside the gamut of the primaries.
whiteVInPrimary := e.invPrimaryColors.Multiplied(whiteColor)
// The following is just a question of how to weight the different values.
// We want to increase the CRI and light output.
// The primary values are decreased as the whites are increased, in a way that doesn't change the total luminance or color output.
whiteScaling, err := primaryV.ScaledToPositiveDifference(whiteVInPrimary)
if err != nil {
panic(err) // Shouldn't happen.
}
primaryV, err = primaryV.Sum(whiteVInPrimary.Scaled(-whiteScaling))
if err != nil {
panic(err) // Shouldn't happen.
}
whiteV = whiteV.Scaled(whiteScaling)
}
// Put all values into one slice.
linV := make(LinDCSVector, 0, primaryV.Channels()+whiteV.Channels())
linV = append(append(linV, primaryV...), whiteV...)
// Prevent any channel from clipping in a way that doesn't change the color.
linV = linV.ClampedUniform()
// Limit output.
if e.OutputLimiter != nil {
linV = e.OutputLimiter.LimitDCS(linV)
}
// Clamp values, apply transfer function.
nonLinV := linV.ClampedAndDeLinearized(e.TransferFunc)
return nonLinV
}
// DCSToXYZ takes a vector from the device color space and returns the color it represents.
//
// Short: Device color space --> XYZ.
func (e *ColorProfileGeneral) DCSToXYZ(v DCSVector) (CIE1931XYZAbs, error) {
if v.Channels() != e.Channels() {
return CIE1931XYZAbs{}, fmt.Errorf("unexpected number of channels. Got %d, want %d", v.Channels(), e.Channels())
}
linV := v.ClampedAndLinearized(e.TransferFunc)
if e.OutputLimiter != nil {
linV = e.OutputLimiter.LimitDCS(linV)
}
// Calculate resulting color.
// This is just the linear combination of all column vectors of the transformation matrix.
result := CIE1931XYZAbs{}
if color, err := e.FullTransformation().Multiplied(linV); err != nil {
return CIE1931XYZAbs{}, fmt.Errorf("failed to multiply transformation matrix with a linear device color space vector: %w", err)
} else {
result = result.Sum(color)
}
return result, nil
}
func (e *ColorProfileGeneral) TransferFunction() TransferFunction {
return e.TransferFunc
}
// NoWhiteOptimizationColorProfile returns a copy of this color profile with all high CRI white emitters disabled.
//
// The output of such profile can not optimize for high CRI and high luminance.
// This will cause that the emitted color is only constructed by the primary colors (e.g. red, green, blue).
// This will not change the emitted color, but the maximum brightness may be reduced and the CRI is not as good as it could be.
// One use-case could be to eliminate any timing discrepancy between high CRI whites and primary color emitters, as these two classes of emitters may be filtered (by a low-pass) in a different way.
//
// Some color profiles do not support this and will just return themselves.
func (e *ColorProfileGeneral) NoWhiteOptimizationColorProfile() ColorProfile {
if e.noWhiteOptimizationColorProfile != nil {
return e.noWhiteOptimizationColorProfile
}
// Fall back to itself.
return e
}