-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcolors.go
206 lines (177 loc) · 5.83 KB
/
colors.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
package ortfodb
import (
"fmt"
"image"
"image/color"
"image/gif"
"os"
"strings"
"time"
"github.com/EdlinOrg/prominentcolor"
ll "github.com/ewen-lbh/label-logger-go"
"github.com/lucasb-eyer/go-colorful"
"github.com/zyedidia/generic/mapset"
_ "golang.org/x/image/webp"
)
// ColorPalette reprensents the object in a Work's metadata.colors.
type ColorPalette struct {
Primary string `json:"primary"`
Secondary string `json:"secondary"`
Tertiary string `json:"tertiary"`
}
func (colors ColorPalette) Empty() bool {
return colors.Primary == "" && colors.Secondary == "" && colors.Tertiary == ""
}
// MergeWith merges the colors of the current palette with other: if a color is missing in the current palette, it is
// replaced by the one in other.
func (colors ColorPalette) MergeWith(other ColorPalette) ColorPalette {
merged := colors
if merged.Primary == "" {
merged.Primary = other.Primary
}
if merged.Secondary == "" {
merged.Secondary = other.Secondary
}
if merged.Tertiary == "" {
merged.Tertiary = other.Tertiary
}
return merged
}
// SortBySaturation sorts the palette by saturation. Primary will be the most saturated, tertiary the least.
// Empty or invalid colors are treated as having 0 saturation.
func (colors *ColorPalette) SortBySaturation() {
primary := colors.Primary
secondary := colors.Secondary
tertiary := colors.Tertiary
ll.Debug("sorting colors based on saturations: primary(%s) = %f, secondary(%s) = %f, tertiary(%s) = %f", primary, saturation(primary), secondary, saturation(secondary), tertiary, saturation(tertiary))
if saturation(primary) < saturation(secondary) {
primary, secondary = secondary, primary
}
if saturation(primary) < saturation(tertiary) {
primary, tertiary = tertiary, primary
}
if saturation(secondary) < saturation(tertiary) {
secondary, tertiary = tertiary, secondary
}
colors.Primary = primary
colors.Secondary = secondary
colors.Tertiary = tertiary
}
// mostSaturated returns at most n colors, sorted by descending saturation.
func paletteFromMostSaturated(colors mapset.Set[color.Color]) ColorPalette {
bySaturation := make(map[string]float64, 0)
colors.Each(func(color color.Color) {
r, g, b, _ := color.RGBA()
hex := colorful.Color{R: float64(r) / float64(0xffff), G: float64(g) / float64(0xffff), B: float64(b) / float64(0xffff)}.Hex()
bySaturation[hex] = saturation(hex)
})
ll.Debug("paletteFromMostSaturated: bySaturation = %v", bySaturation)
leastSaturatedSaturation := 0.0
mostSaturateds := make([]string, 3)
for hex, sat := range bySaturation {
if sat > leastSaturatedSaturation {
mostSaturateds[0], mostSaturateds[1], mostSaturateds[2] = hex, mostSaturateds[0], mostSaturateds[1]
leastSaturatedSaturation = sat
}
}
ll.Debug("paletteFromMostSaturated: mostSaturateds = %v", mostSaturateds)
return ColorPalette{
Primary: mostSaturateds[0],
Secondary: mostSaturateds[1],
Tertiary: mostSaturateds[2],
}
}
// saturation returns the saturation of the given colorstring.
// invalid or empty colorstrings return 0.
func saturation(colorstring string) float64 {
color, err := colorful.Hex(colorstring)
if err != nil {
return 0
}
_, sat, _ := color.Hsv()
return sat
}
func canExtractColors(contentType string) bool {
switch strings.Split(contentType, "/")[1] {
case "jpeg", "png", "webp", "pbm", "ppm", "pgm", "gif":
return true
default:
return false
}
}
// ExtractColors extracts the 3 most proeminent colors from the given image-decodable file.
// See https://pkg.go.dev/image#Decode for what formats are decodable.
func ExtractColors(filename string, contentType string) (ColorPalette, error) {
defer ll.TimeTrack(time.Now(), "ExtractColors", filename)
file, err := os.Open(filename)
if err != nil {
return ColorPalette{}, err
}
defer file.Close()
if contentType == "image/gif" {
ll.Debug("extract colors from %s: decoding gif", filename)
var decodedGif *gif.GIF
decodedGif, err = gif.DecodeAll(file)
if err != nil {
return ColorPalette{}, fmt.Errorf("could not decode gif's config: %w", err)
}
gifColorsWithAppearanceCount := make(map[color.Color]int)
for _, frame := range decodedGif.Image {
for _, paletteIndex := range frame.Pix {
gifColorsWithAppearanceCount[frame.Palette[paletteIndex]]++
}
}
averageAppearanceCount := 0
for _, count := range gifColorsWithAppearanceCount {
averageAppearanceCount += count
}
averageAppearanceCount /= len(gifColorsWithAppearanceCount)
gifColors := mapset.New[color.Color]()
for color, count := range gifColorsWithAppearanceCount {
if count > averageAppearanceCount/5 {
gifColors.Put(color)
}
}
ll.Debug("extract colors from %s: extracting most saturated colors from %d unique colors: %v", filename, gifColors.Size(), gifColorsWithAppearanceCount)
return paletteFromMostSaturated(gifColors), nil
}
img, _, err := image.Decode(file)
if err != nil {
return ColorPalette{}, err
}
return kmeans(img)
}
// kmeans extracts colors from img.
func kmeans(img image.Image) (ColorPalette, error) {
centroids, err := prominentcolor.Kmeans(img)
if err != nil {
// retry without masking out backgrounds or cropping
centroids, err = prominentcolor.KmeansWithAll(prominentcolor.DefaultK, img, prominentcolor.ArgumentNoCropping, prominentcolor.DefaultSize, []prominentcolor.ColorBackgroundMask{})
if err != nil {
return ColorPalette{}, err
}
}
colors := make([]string, 0)
for _, centroid := range centroids {
colors = append(colors, centroid.AsString())
}
if len(colors) == 0 {
return ColorPalette{}, fmt.Errorf("no colors found in given image")
}
primary := "#" + colors[0]
secondary := ""
tertiary := ""
if len(colors) > 1 {
secondary = "#" + colors[1]
}
if len(colors) > 2 {
tertiary = "#" + colors[2]
}
palette := ColorPalette{
Primary: primary,
Secondary: secondary,
Tertiary: tertiary,
}
palette.SortBySaturation()
return palette, nil
}