g-spot-michael 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/g_spot_michael.rb +305 -0
- metadata +46 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4c41e23eece9dd00540a6de977823c9936e6d9bc
|
4
|
+
data.tar.gz: f9abc425f3f47e7c99e610a9dc96296760fd2b19
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e52bf05e32a9ff32f29b8f4b05a0029803644b54ed95af4b1d023cc37b811417ebffaf2ee456e14a07e400bed48e129ec9f610a9d6e8c6d32a0af57954535091
|
7
|
+
data.tar.gz: b4bead399c6ae4e421b653e02318f4e0059fe02095673fa351f022a6b1264ae2e4b8372f412166a1118d5abb06b070079c4d2aeb80b3315954ec43c3b5f11ad7
|
@@ -0,0 +1,305 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
class GSpotMichael
|
5
|
+
|
6
|
+
TARGET_DARK_LUMA = 0.26
|
7
|
+
MAX_DARK_LUMA = 0.45
|
8
|
+
MIN_LIGHT_LUMA = 0.55
|
9
|
+
TARGET_LIGHT_LUMA = 0.74
|
10
|
+
|
11
|
+
MIN_NORMAL_LUMA = 0.3
|
12
|
+
TARGET_NORMAL_LUMA = 0.5
|
13
|
+
MAX_NORMAL_LUMA = 0.7
|
14
|
+
|
15
|
+
TARGET_MUTED_SATURATION = 0.3
|
16
|
+
MAX_MUTED_SATURATION = 0.4
|
17
|
+
|
18
|
+
TARGET_VIBRANT_SATURATION = 1
|
19
|
+
MIN_VIBRANT_SATURATION = 0.35
|
20
|
+
|
21
|
+
WEIGHT_SATURATION = 3
|
22
|
+
WEIGHT_LUMA = 6
|
23
|
+
WEIGHT_POPULATION = 1
|
24
|
+
|
25
|
+
def initialize(file, colorCount=64, quality='5%')
|
26
|
+
@HighestPopulation = 0
|
27
|
+
|
28
|
+
# we're going to take the original file,
|
29
|
+
# reduce the size, quantize the colors,
|
30
|
+
# and then return an array of distinct
|
31
|
+
# RGBs with their occurence counts(as pairs)
|
32
|
+
file.rewind
|
33
|
+
results = run_command "convert - -scale '#{quality}' +dither -colors #{colorCount} txt:", file.read.force_encoding("UTF-8")
|
34
|
+
pixels = results.split("\n").map{|x| (r = x.match(/srgb\((\d{1,3},\d{1,3},\d{1,3})\)/)) ? r[1] : nil}.compact.map{|p| p.split(",").map(&:to_i)}
|
35
|
+
cmap = {}
|
36
|
+
pixels.each do |pixel|
|
37
|
+
# If pixel is mostly opaque and not white
|
38
|
+
if !(pixel[0] > 250 && pixel[1] > 250 && pixel[2] > 250)
|
39
|
+
cmap[pixel] ||= 0
|
40
|
+
cmap[pixel] += 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@swatches = cmap.map do |vbox|
|
45
|
+
Swatch.new vbox[0], vbox[1] # values, count
|
46
|
+
end
|
47
|
+
|
48
|
+
@maxPopulation = find_max_population
|
49
|
+
@HighestPopulation = @maxPopulation
|
50
|
+
|
51
|
+
generate_variation_colors
|
52
|
+
generate_empty_swatches
|
53
|
+
end
|
54
|
+
|
55
|
+
def generate_variation_colors
|
56
|
+
@VibrantSwatch = find_color_variation(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1)
|
57
|
+
@LightVibrantSwatch = find_color_variation(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1)
|
58
|
+
@DarkVibrantSwatch = find_color_variation(TARGET_DARK_LUMA, 0, MAX_DARK_LUMA, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1)
|
59
|
+
@MutedSwatch = find_color_variation(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA, TARGET_MUTED_SATURATION, 0, MAX_MUTED_SATURATION)
|
60
|
+
@LightMutedSwatch = find_color_variation(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1, TARGET_MUTED_SATURATION, 0, MAX_MUTED_SATURATION)
|
61
|
+
@DarkMutedSwatch = find_color_variation(TARGET_DARK_LUMA, 0, MAX_DARK_LUMA, TARGET_MUTED_SATURATION, 0, MAX_MUTED_SATURATION)
|
62
|
+
end
|
63
|
+
|
64
|
+
def generate_empty_swatches
|
65
|
+
if !@VibrantSwatch
|
66
|
+
# If we do not have a vibrant color...
|
67
|
+
if @DarkVibrantSwatch
|
68
|
+
# ...but we do have a dark vibrant, generate the value by modifying the luma
|
69
|
+
hsl = @DarkVibrantSwatch.hsl
|
70
|
+
hsl[2] = TARGET_NORMAL_LUMA
|
71
|
+
@VibrantSwatch = Swatch.new hslToRgb(hsl[0], hsl[1], hsl[2]), 0
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if !@DarkVibrantSwatch
|
76
|
+
# If we do not have a vibrant color...
|
77
|
+
if @VibrantSwatch
|
78
|
+
# ...but we do have a dark vibrant, generate the value by modifying the luma
|
79
|
+
hsl = @VibrantSwatch.hsl
|
80
|
+
hsl[2] = TARGET_DARK_LUMA
|
81
|
+
@DarkVibrantSwatch = Swatch.new hslToRgb(hsl[0], hsl[1], hsl[2]), 0
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def find_max_population
|
87
|
+
population = @swatches.map{|swatch| [0, swatch.population].max }.max
|
88
|
+
population
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_color_variation (targetLuma, minLuma, maxLuma, targetSaturation, minSaturation, maxSaturation)
|
92
|
+
max = nil
|
93
|
+
maxValue = 0
|
94
|
+
|
95
|
+
@swatches.each do |swatch|
|
96
|
+
sat = swatch.hsl[1]
|
97
|
+
luma = swatch.hsl[2]
|
98
|
+
if !is_already_selected(swatch) && sat >= minSaturation && sat <= maxSaturation && luma >= minLuma && luma <= maxLuma
|
99
|
+
value = create_comparison_value sat, targetSaturation, luma, targetLuma, swatch.population, @HighestPopulation
|
100
|
+
if !max || value > maxValue
|
101
|
+
max = swatch
|
102
|
+
maxValue = value
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
max
|
108
|
+
end
|
109
|
+
|
110
|
+
def create_comparison_value(saturation, targetSaturation, luma, targetLuma, population, maxPopulation)
|
111
|
+
# pop = (maxPopulation != 0) ? (population / maxPopulation) : 0.0
|
112
|
+
weighted_mean(
|
113
|
+
invert_diff(saturation, targetSaturation), WEIGHT_SATURATION,
|
114
|
+
invert_diff(luma, targetLuma), WEIGHT_LUMA,
|
115
|
+
(population / maxPopulation), WEIGHT_POPULATION
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
def invert_diff (value, targetValue)
|
120
|
+
1 - (value - targetValue).abs
|
121
|
+
end
|
122
|
+
|
123
|
+
def weighted_mean(*values)
|
124
|
+
sum = 0
|
125
|
+
sumWeight = 0
|
126
|
+
i = 0
|
127
|
+
while i < values.length
|
128
|
+
value = values[i]
|
129
|
+
weight = values[i + 1]
|
130
|
+
sum += value * weight
|
131
|
+
sumWeight += weight
|
132
|
+
i += 2
|
133
|
+
end
|
134
|
+
sum / sumWeight
|
135
|
+
end
|
136
|
+
|
137
|
+
def swatches
|
138
|
+
{
|
139
|
+
Vibrant: @VibrantSwatch,
|
140
|
+
Muted: @MutedSwatch,
|
141
|
+
DarkVibrant: @DarkVibrantSwatch,
|
142
|
+
DarkMuted: @DarkMutedSwatch,
|
143
|
+
LightVibrant: @LightVibrantSwatch,
|
144
|
+
LightMuted: @LightMuted
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
def is_already_selected(swatch)
|
149
|
+
@VibrantSwatch == swatch ||
|
150
|
+
@DarkVibrantSwatch == swatch ||
|
151
|
+
@LightVibrantSwatch == swatch ||
|
152
|
+
@MutedSwatch == swatch ||
|
153
|
+
@DarkMutedSwatch == swatch ||
|
154
|
+
@LightMutedSwatch == swatch
|
155
|
+
end
|
156
|
+
|
157
|
+
def hslToRgb(h, s, l)
|
158
|
+
r = nil
|
159
|
+
g = nil
|
160
|
+
b = nil
|
161
|
+
|
162
|
+
hue2rgb = Proc.new { |p, q, t|
|
163
|
+
if t < 0
|
164
|
+
t += 1
|
165
|
+
end
|
166
|
+
if t > 1
|
167
|
+
t -= 1
|
168
|
+
end
|
169
|
+
if t < 1 / 6
|
170
|
+
return p + (q - p) * 6 * t
|
171
|
+
end
|
172
|
+
if t < 1 / 2
|
173
|
+
return q
|
174
|
+
end
|
175
|
+
if t < 2 / 3
|
176
|
+
return p + (q - p) * (2 / 3 - t) * 6
|
177
|
+
end
|
178
|
+
p
|
179
|
+
}
|
180
|
+
|
181
|
+
if s == 0
|
182
|
+
r = g = b = l
|
183
|
+
# achromatic
|
184
|
+
else
|
185
|
+
q = (l < 0.5) ? (l * (1 + s)) : (l + s - (l * s))
|
186
|
+
p = 2 * l - q
|
187
|
+
r = hue2rgb.call(p, q, h + 1 / 3)
|
188
|
+
g = hue2rgb.call(p, q, h)
|
189
|
+
b = hue2rgb.call(p, q, h - (1 / 3))
|
190
|
+
end
|
191
|
+
[
|
192
|
+
r * 255,
|
193
|
+
g * 255,
|
194
|
+
b * 255
|
195
|
+
]
|
196
|
+
end
|
197
|
+
|
198
|
+
def run_command command, input
|
199
|
+
stdin, stdout, stderr, wait_thr = Open3.popen3(command)
|
200
|
+
pid = wait_thr.pid
|
201
|
+
|
202
|
+
Timeout.timeout(10) do # cancel in 10 seconds
|
203
|
+
stdin.write input
|
204
|
+
stdin.close
|
205
|
+
|
206
|
+
output_buffer = []
|
207
|
+
error_buffer = []
|
208
|
+
|
209
|
+
while (output_chunk = stdout.gets) || (error_chunk = stderr.gets)
|
210
|
+
output_buffer << output_chunk
|
211
|
+
error_buffer << error_chunk
|
212
|
+
end
|
213
|
+
|
214
|
+
output_buffer.compact!
|
215
|
+
error_buffer.compact!
|
216
|
+
|
217
|
+
output = output_buffer.any? ? output_buffer.join('') : nil
|
218
|
+
error = error_buffer.any? ? error_buffer.join('') : nil
|
219
|
+
|
220
|
+
unless error
|
221
|
+
raise StandardError.new("No output received.") if !output
|
222
|
+
return output
|
223
|
+
else
|
224
|
+
raise StandardError.new(error)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
rescue Timeout::Error, StandardError, Errno::EPIPE => e
|
228
|
+
e
|
229
|
+
ensure
|
230
|
+
begin
|
231
|
+
Process.kill("KILL", pid) if pid
|
232
|
+
rescue Errno::ESRCH
|
233
|
+
# Process is already dead so do nothing.
|
234
|
+
end
|
235
|
+
stdin = nil
|
236
|
+
stdout = nil
|
237
|
+
stderr = nil
|
238
|
+
wait_thr.value if wait_thr # Process::Status object returned.
|
239
|
+
end
|
240
|
+
|
241
|
+
class Swatch
|
242
|
+
|
243
|
+
attr_accessor :rgb, :population
|
244
|
+
|
245
|
+
def initialize(rgb, population=1)
|
246
|
+
@rgb = rgb
|
247
|
+
@population = population
|
248
|
+
@yiq = 0
|
249
|
+
end
|
250
|
+
|
251
|
+
def hsl
|
252
|
+
@hsl ||= rgb_to_hsl @rgb[0], @rgb[1], @rgb[2]
|
253
|
+
end
|
254
|
+
|
255
|
+
def hex
|
256
|
+
"#" + ((1 << 24) + (@rgb[0] << 16) + (@rgb[1] << 8) + @rgb[2]).to_s(16).slice(1, 7)
|
257
|
+
end
|
258
|
+
|
259
|
+
def get_title_text_color
|
260
|
+
ensure_text_colors
|
261
|
+
(@yiq < 200) ? "#fff" : "#000"
|
262
|
+
end
|
263
|
+
|
264
|
+
def get_body_text_color
|
265
|
+
ensure_text_colors
|
266
|
+
(@yiq < 150) ? "#fff" : "#000"
|
267
|
+
end
|
268
|
+
|
269
|
+
private
|
270
|
+
|
271
|
+
def ensure_text_colors
|
272
|
+
if @yiq != 0
|
273
|
+
@yiq = (@rgb[0] * 299 + @rgb[1] * 587 + @rgb[2] * 114) / 1000
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def rgb_to_hsl(r, g, b)
|
278
|
+
r = r.to_f / 255.0
|
279
|
+
g = g.to_f / 255.0
|
280
|
+
b = b.to_f / 255.0
|
281
|
+
max = [r, g, b].max
|
282
|
+
min = [r, g, b].min
|
283
|
+
h = 0.0
|
284
|
+
s = 0.0
|
285
|
+
l = (max + min) / 2
|
286
|
+
unless max == min # if not achromatic
|
287
|
+
d = max - min
|
288
|
+
s = (l > 0.5) ? (d / (2 - max - min)) : (d / (max + min))
|
289
|
+
case max
|
290
|
+
when r
|
291
|
+
h = (g - b) / d + ((g < b) ? 6 : 0)
|
292
|
+
when g
|
293
|
+
h = (b - r) / d + 2
|
294
|
+
when b
|
295
|
+
h = (r - g) / d + 4
|
296
|
+
end
|
297
|
+
h = h / 6
|
298
|
+
end
|
299
|
+
[h, s, l]
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
|
metadata
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: g-spot-michael
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben Titcomb
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-04-07 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A Ruby port of Vibrant.js using Imagemagick. Extracts prominent colors
|
14
|
+
from a provided image.
|
15
|
+
email: benjamin@pixelstreetinc.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/g_spot_michael.rb
|
21
|
+
homepage: https://github.com/ravenstine/g-spot-michael
|
22
|
+
licenses:
|
23
|
+
- MIT
|
24
|
+
metadata: {}
|
25
|
+
post_install_message:
|
26
|
+
rdoc_options: []
|
27
|
+
require_paths:
|
28
|
+
- lib
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
requirements: []
|
40
|
+
rubyforge_project:
|
41
|
+
rubygems_version: 2.5.1
|
42
|
+
signing_key:
|
43
|
+
specification_version: 4
|
44
|
+
summary: A Ruby port of Vibrant.js using Imagemagick
|
45
|
+
test_files: []
|
46
|
+
has_rdoc:
|