-
Notifications
You must be signed in to change notification settings - Fork 3
/
palette.go
316 lines (277 loc) · 9.59 KB
/
palette.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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
package vibrant
import (
"golang.org/x/image/draw"
"image"
"log"
)
// Defaults used by the default PaletteBuilder.
const (
DefaultResizeArea = uint64(320 * 320)
DefaultMaximumColorCount = uint32(64)
)
// Defaults used by the default PaletteBuilder.
var (
DefaultScaler = draw.ApproxBiLinear
)
// Palette extracts prominent colors from an image.
//
// A number of colors with different profiles are extracted from the image:
//
// Vibrant
// Vibrant Dark
// Vibrant Light
// Muted
// Muted Dark
// Muted Light
//
// These can be retrieved via the appropriate getter method.
//
// Instances are created with a PaletteBuilder which supports several options to tweak the generated Palette.
type Palette struct {
swatches []*Swatch
targets []*Target
selectedSwatches map[*Target]*Swatch
usedColors map[uint32]bool
maxPopulation uint32
}
func newPalette(swatches []*Swatch, targets []*Target) *Palette {
maxPopulation := uint32(0)
for _, swatch := range swatches {
if swatch.Population() > maxPopulation {
maxPopulation = swatch.Population()
}
}
return &Palette{
swatches: swatches,
targets: targets,
selectedSwatches: make(map[*Target]*Swatch),
usedColors: make(map[uint32]bool),
maxPopulation: maxPopulation,
}
}
// Returns all of the swatches which make up the palette.
func (p *Palette) Swatches() []*Swatch {
return p.swatches
}
// Returns the targets used to generate this palette.
func (p *Palette) Targets() []*Target {
return p.targets
}
// Returns the most vibrant swatch in the palette. Might be nil.
func (p *Palette) VibrantSwatch() *Swatch {
return p.SwatchForTarget(Vibrant)
}
// Returns a light and vibrant swatch from the palette. Might be nil.
func (p *Palette) LightVibrantSwatch() *Swatch {
return p.SwatchForTarget(LightVibrant)
}
// Returns a dark and vibrant swatch from the palette. Might be nil.
func (p *Palette) DarkVibrantSwatch() *Swatch {
return p.SwatchForTarget(DarkVibrant)
}
// Returns a muted swatch from the palette. Might be nil.
func (p *Palette) MutedSwatch() *Swatch {
return p.SwatchForTarget(Muted)
}
// Returns a muted and light swatch from the palette. Might be nil.
func (p *Palette) LightMutedSwatch() *Swatch {
return p.SwatchForTarget(LightMuted)
}
// Returns a muted and dark swatch from the palette. Might be nil.
func (p *Palette) DarkMutedSwatch() *Swatch {
return p.SwatchForTarget(DarkMuted)
}
// Returns the most vibrant color in the palette as an RGB packed int.
func (p *Palette) VibrantColor(defaultColor uint32) uint32 {
return p.ColorForTarget(Vibrant, defaultColor)
}
// Returns a light and vibrant color from the palette as an RGB packed int.
func (p *Palette) LightVibrantColor(defaultColor uint32) uint32 {
return p.ColorForTarget(LightVibrant, defaultColor)
}
// Returns a dark and vibrant color from the palette as an RGB packed int.
func (p *Palette) DarkVibrantColor(defaultColor uint32) uint32 {
return p.ColorForTarget(DarkVibrant, defaultColor)
}
// Returns a muted color from the palette as an RGB packed int.
func (p *Palette) MutedColor(defaultColor uint32) uint32 {
return p.ColorForTarget(Muted, defaultColor)
}
// Returns a muted and light color from the palette as an RGB packed int.
func (p *Palette) LightMutedColor(defaultColor uint32) uint32 {
return p.ColorForTarget(LightMuted, defaultColor)
}
// Returns a muted and dark color from the palette as an RGB packed int.
func (p *Palette) DarkMutedColor(defaultColor uint32) uint32 {
return p.ColorForTarget(DarkMuted, defaultColor)
}
// Returns the selected swatch for the given target from the palette, or nil if one
// could not be found.
func (p *Palette) SwatchForTarget(target *Target) *Swatch {
return p.selectedSwatches[target]
}
// Returns the selected color for the given target from the palette as an RGB packed int.
func (p *Palette) ColorForTarget(target *Target, defaultColor uint32) uint32 {
swatch := p.SwatchForTarget(target)
if swatch != nil {
return swatch.RGBAInt().PackedRGB()
}
return defaultColor
}
func (p *Palette) generate() {
// We need to make sure that the scored targets are generated first. This is so that
// inherited targets have something to inherit from
for _, target := range p.targets {
p.selectedSwatches[target] = p.generateScoredTarget(target)
}
// We now clear out the used colors
p.usedColors = make(map[uint32]bool)
}
func (p *Palette) generateScoredTarget(target *Target) *Swatch {
maxScoreSwatch := p.maxScoredSwatchForTarget(target)
if maxScoreSwatch != nil && target.IsExclusive() {
// If we have a swatch, and the target is exclusive, add the color to the used list
p.usedColors[maxScoreSwatch.RGBAInt().PackedRGB()] = true
}
return maxScoreSwatch
}
func (p *Palette) maxScoredSwatchForTarget(target *Target) *Swatch {
scorer := target.Scorer(p.swatches)
maxScore := 0.0
var maxScoreSwatch *Swatch
for _, swatch := range p.swatches {
if p.shouldBeScoredForTarget(swatch, target) {
score := scorer.Score(swatch)
if maxScoreSwatch == nil || score > maxScore {
maxScoreSwatch = swatch
maxScore = score
}
}
}
return maxScoreSwatch
}
func (p *Palette) shouldBeScoredForTarget(swatch *Swatch, target *Target) bool {
// Check whether the HSL values are within the correct ranges, and this color hasn't been used yet.
hsl := swatch.HSL()
return !p.usedColors[swatch.RGBAInt().PackedRGB()] &&
hsl.S >= target.MinimumSaturation() && hsl.S <= target.MaximumSaturation() &&
hsl.L >= target.MinimumLightness() && hsl.L <= target.MaximumLightness()
}
// PaletteBuilder is used for generating Palette instances.
type PaletteBuilder struct {
image image.Image
region image.Rectangle
swatches []*Swatch
targets []*Target
filters []Filter
maximumColorCount uint32
resizeArea uint64
scaler draw.Scaler
}
// NewPaletteBuilder creates a new PaletteBuilder for an image.
func NewPaletteBuilder(image image.Image) *PaletteBuilder {
return &PaletteBuilder{
image: image,
region: image.Bounds(),
swatches: make([]*Swatch, 0, DefaultMaximumColorCount),
targets: []*Target{Vibrant, Muted, LightVibrant, LightMuted, DarkVibrant, DarkMuted},
filters: []Filter{DefaultFilter},
maximumColorCount: DefaultMaximumColorCount,
resizeArea: DefaultResizeArea,
scaler: DefaultScaler,
}
}
// Set the maximum number of colors to use in the quantization step.
//
// Good values for depend on the source image type. For landscapes, good values are in
// the range 10-16. For images which are largely made up of people's faces then this
// value should be increased to ~24.
func (b *PaletteBuilder) MaximumColorCount(colors uint32) *PaletteBuilder {
b.maximumColorCount = colors
return b
}
// Set the resize value. If the image's area is greater than the value specified, then
// the image will be resized so that it's area matches the provided area. If the
// image is smaller or equal, the original is used as-is.
//
// This value has a large effect on the processing time. The larger the resized image is,
// the greater time it will take to generate the palette. The smaller the image is, the
// more detail is lost in the resulting image and thus less precision for color selection.
//
// A value of 0 can be used to disable resizing.
func (b *PaletteBuilder) ResizeImageArea(area uint64) *PaletteBuilder {
b.resizeArea = area
return b
}
// Specify the scaling function used to resize an image. Set to nil to disable resizing.
func (b *PaletteBuilder) Scaler(scaler draw.Scaler) *PaletteBuilder {
b.scaler = scaler
return b
}
// Set a region of the image to be used exclusively when calculating the palette.
func (b *PaletteBuilder) Region(region image.Rectangle) *PaletteBuilder {
if b.image != nil {
b.region = region
if !b.region.In(b.image.Bounds()) {
log.Panicln("The given region must be within the image's dimensions.")
}
}
return b
}
// Clear a previously specified region.
func (b *PaletteBuilder) ClearRegion() *PaletteBuilder {
b.region = b.image.Bounds()
return b
}
// Add a filter to be able to have fine grained control over which colors are
// allowed in the resulting palette.
func (b *PaletteBuilder) AddFilter(filter Filter) *PaletteBuilder {
if filter != nil {
b.filters = append(b.filters, filter)
}
return b
}
// Clear all added filters. This includes any default filters added automatically.
func (b *PaletteBuilder) ClearFilters() *PaletteBuilder {
b.filters = make([]Filter, 0)
return b
}
// Add a target profile to be generated in the palette.
//
// You can retrieve the result via Palette#getSwatchForTarget(Target).
func (b *PaletteBuilder) AddTarget(target *Target) *PaletteBuilder {
shouldAppend := true
for _, t := range b.targets {
if t == target {
shouldAppend = false
break
}
}
if shouldAppend {
b.targets = append(b.targets, target)
}
return b
}
// Clear all added targets. This includes any default targets added automatically.
func (b *PaletteBuilder) ClearTargets() *PaletteBuilder {
b.targets = make([]*Target, 0)
return b
}
// Generate and return the Palette.
func (b *PaletteBuilder) Generate() *Palette {
if b.image != nil {
// Select the region of the image if possible...
m := b.image
if _, ok := b.image.(SubImager); ok {
m = m.(SubImager).SubImage(b.region)
}
// Next we scale the image if necessary...
m = ScaleImageDown(m, b.resizeArea, b.scaler)
// Now generate swatches from the image...
quantizer := NewColorCutQuantizerWithFilters(b.filters)
b.swatches = quantizer.Swatches(b.maximumColorCount, m)
}
palette := newPalette(b.swatches, b.targets)
palette.generate()
return palette
}