g-spot-michael 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|