colorscore 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of colorscore might be problematic. Click here for more details.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +7 -0
- data/colorscore.gemspec +20 -0
- data/lib/colorscore.rb +6 -0
- data/lib/colorscore/histogram.rb +24 -0
- data/lib/colorscore/metrics.rb +141 -0
- data/lib/colorscore/palette.rb +36 -0
- data/lib/colorscore/version.rb +3 -0
- data/test/fixtures/skydiver.jpg +0 -0
- data/test/histogram_test.rb +12 -0
- data/test/metrics_test.rb +13 -0
- data/test/palette_test.rb +13 -0
- data/test/test_helper.rb +3 -0
- metadata +110 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
data/colorscore.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "colorscore/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "colorscore"
|
7
|
+
s.version = Colorscore::VERSION
|
8
|
+
s.authors = ["Milo Winningham"]
|
9
|
+
s.email = ["milo@winningham.net"]
|
10
|
+
s.summary = %q{Finds the dominant colors in an image.}
|
11
|
+
s.description = %q{Finds the dominant colors in an image and scores them against a user-defined palette, using the CIE2000 Delta E formula.}
|
12
|
+
|
13
|
+
s.add_dependency "color"
|
14
|
+
s.add_development_dependency "rake"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
end
|
data/lib/colorscore.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Colorscore
|
2
|
+
class Histogram
|
3
|
+
def initialize(image_path, colors=16, depth=8)
|
4
|
+
output = `convert #{image_path} -resize 400x400 -format %c -dither None -quantize LAB -colors #{colors} -depth #{depth} histogram:info:-`
|
5
|
+
@lines = output.lines.sort.reverse.map(&:strip).reject(&:empty?)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns an array of colors in descending order of occurances.
|
9
|
+
def colors
|
10
|
+
hex_values = @lines.map { |line| line[/#[0-9A-F]+/] }
|
11
|
+
hex_values.map { |hex| Color::RGB.from_html(*hex) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def color_counts
|
15
|
+
@lines.map { |line| line.split(':')[0].to_i }
|
16
|
+
end
|
17
|
+
|
18
|
+
def scores
|
19
|
+
total = color_counts.inject(:+).to_f
|
20
|
+
scores = color_counts.map { |count| count / total }
|
21
|
+
scores.zip(colors)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Colorscore
|
2
|
+
module Metrics
|
3
|
+
def self.similarity(a, b)
|
4
|
+
1 - distance(a, b)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.distance(color_1, color_2)
|
8
|
+
l1, a1, b1 = xyz_to_lab(*rgb_to_xyz(color_1))
|
9
|
+
l2, a2, b2 = xyz_to_lab(*rgb_to_xyz(color_2))
|
10
|
+
|
11
|
+
distance = delta_e_cie_2000(l1, a1, b1, l2, a2, b2)
|
12
|
+
scale(distance, 0..100)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Ported from colormath for Python.
|
16
|
+
def self.delta_e_cie_2000(l1, a1, b1, l2, a2, b2)
|
17
|
+
kl = kc = kh = 1
|
18
|
+
|
19
|
+
avg_lp = (l1 + l2) / 2.0
|
20
|
+
c1 = Math.sqrt((a1 ** 2) + (b1 ** 2))
|
21
|
+
c2 = Math.sqrt((a2 ** 2) + (b2 ** 2))
|
22
|
+
avg_c1_c2 = (c1 + c2) / 2.0
|
23
|
+
|
24
|
+
g = 0.5 * (1 - Math.sqrt((avg_c1_c2 ** 7.0) / ((avg_c1_c2 ** 7.0) + (25.0 ** 7.0))))
|
25
|
+
|
26
|
+
a1p = (1.0 + g) * a1
|
27
|
+
a2p = (1.0 + g) * a2
|
28
|
+
c1p = Math.sqrt((a1p ** 2) + (b1 ** 2))
|
29
|
+
c2p = Math.sqrt((a2p ** 2) + (b2 ** 2))
|
30
|
+
avg_c1p_c2p = (c1p + c2p) / 2.0
|
31
|
+
|
32
|
+
if degrees(Math.atan2(b1,a1p)) >= 0
|
33
|
+
h1p = degrees(Math.atan2(b1,a1p))
|
34
|
+
else
|
35
|
+
h1p = degrees(Math.atan2(b1,a1p)) + 360
|
36
|
+
end
|
37
|
+
|
38
|
+
if degrees(Math.atan2(b2,a2p)) >= 0
|
39
|
+
h2p = degrees(Math.atan2(b2,a2p))
|
40
|
+
else
|
41
|
+
h2p = degrees(Math.atan2(b2,a2p)) + 360
|
42
|
+
end
|
43
|
+
|
44
|
+
if (h1p - h2p).abs > 180
|
45
|
+
avg_hp = (h1p + h2p + 360) / 2.0
|
46
|
+
else
|
47
|
+
avg_hp = (h1p + h2p) / 2.0
|
48
|
+
end
|
49
|
+
|
50
|
+
t = 1 - 0.17 * Math.cos(radians(avg_hp - 30)) + 0.24 * Math.cos(radians(2 * avg_hp)) + 0.32 * Math.cos(radians(3 * avg_hp + 6)) - 0.2 * Math.cos(radians(4 * avg_hp - 63))
|
51
|
+
|
52
|
+
diff_h2p_h1p = h2p - h1p
|
53
|
+
if diff_h2p_h1p.abs <= 180
|
54
|
+
delta_hp = diff_h2p_h1p
|
55
|
+
elsif diff_h2p_h1p.abs > 180 && h2p <= h1p
|
56
|
+
delta_hp = diff_h2p_h1p + 360
|
57
|
+
else
|
58
|
+
delta_hp = diff_h2p_h1p - 360
|
59
|
+
end
|
60
|
+
|
61
|
+
delta_lp = l2 - l1
|
62
|
+
delta_cp = c2p - c1p
|
63
|
+
delta_hp = 2 * Math.sqrt(c2p * c1p) * Math.sin(radians(delta_hp) / 2.0)
|
64
|
+
|
65
|
+
s_l = 1 + ((0.015 * ((avg_lp - 50) ** 2)) / Math.sqrt(20 + ((avg_lp - 50) ** 2.0)))
|
66
|
+
s_c = 1 + 0.045 * avg_c1p_c2p
|
67
|
+
s_h = 1 + 0.015 * avg_c1p_c2p * t
|
68
|
+
|
69
|
+
delta_ro = 30 * Math.exp(-((((avg_hp - 275) / 25) ** 2.0)))
|
70
|
+
r_c = Math.sqrt(((avg_c1p_c2p ** 7.0)) / ((avg_c1p_c2p ** 7.0) + (25.0 ** 7.0)));
|
71
|
+
r_t = -2 * r_c * Math.sin(2 * radians(delta_ro))
|
72
|
+
|
73
|
+
delta_e = Math.sqrt(((delta_lp / (s_l * kl)) ** 2) + ((delta_cp / (s_c * kc)) ** 2) + ((delta_hp / (s_h * kh)) ** 2) + r_t * (delta_cp / (s_c * kc)) * (delta_hp / (s_h * kh)))
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.rgb_to_xyz(color)
|
77
|
+
color = color.to_rgb
|
78
|
+
r, g, b = color.r, color.g, color.b
|
79
|
+
|
80
|
+
# assuming sRGB (D65)
|
81
|
+
r = (r <= 0.04045) ? r/12.92 : ((r+0.055)/1.055) ** 2.4
|
82
|
+
g = (g <= 0.04045) ? g/12.92 : ((g+0.055)/1.055) ** 2.4
|
83
|
+
b = (b <= 0.04045) ? b/12.92 : ((b+0.055)/1.055) ** 2.4
|
84
|
+
|
85
|
+
r *= 100
|
86
|
+
g *= 100
|
87
|
+
b *= 100
|
88
|
+
|
89
|
+
x = 0.412453*r + 0.357580*g + 0.180423*b
|
90
|
+
y = 0.212671*r + 0.715160*g + 0.072169*b
|
91
|
+
z = 0.019334*r + 0.119193*g + 0.950227*b
|
92
|
+
|
93
|
+
[x, y, z]
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.xyz_to_lab(x, y, z)
|
97
|
+
x /= 95.047
|
98
|
+
y /= 100.000
|
99
|
+
z /= 108.883
|
100
|
+
|
101
|
+
if x > 0.008856
|
102
|
+
x = x ** (1.0/3)
|
103
|
+
else
|
104
|
+
x = (7.787 * x) + (16.0 / 116)
|
105
|
+
end
|
106
|
+
|
107
|
+
if y > 0.008856
|
108
|
+
y = y ** (1.0/3)
|
109
|
+
else
|
110
|
+
y = (7.787 * y) + (16.0 / 116)
|
111
|
+
end
|
112
|
+
|
113
|
+
if z > 0.008856
|
114
|
+
z = z ** (1.0/3)
|
115
|
+
else
|
116
|
+
z = (7.787 * z) + (16.0 / 116)
|
117
|
+
end
|
118
|
+
|
119
|
+
l = (116.0 * y) - 16.0
|
120
|
+
a = 500.0 * (x - y)
|
121
|
+
b = 200.0 * (y - z)
|
122
|
+
|
123
|
+
[l, a, b]
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.scale(number, from_range, to_range=0..1, clamp=true)
|
127
|
+
if clamp && number <= from_range.begin
|
128
|
+
position = 0
|
129
|
+
elsif clamp && number >= from_range.end
|
130
|
+
position = 1
|
131
|
+
else
|
132
|
+
position = (number - from_range.begin).to_f / (from_range.end - from_range.begin)
|
133
|
+
end
|
134
|
+
|
135
|
+
position * (to_range.end - to_range.begin) + to_range.begin
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.radians(degrees); degrees * Math::PI / 180; end
|
139
|
+
def self.degrees(radians); radians * 180 / Math::PI; end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Colorscore
|
2
|
+
class Palette < Array
|
3
|
+
DEFAULT = ["660000", "990000", "cc0000", "cc3333", "ea4c88", "993399",
|
4
|
+
"663399", "333399", "0066cc", "0099cc", "66cccc", "77cc33",
|
5
|
+
"669900", "336600", "666600", "999900", "cccc33", "ffff00",
|
6
|
+
"ffcc33", "ff9900", "ff6600", "cc6633", "996633", "663300",
|
7
|
+
"000000", "999999", "cccccc", "ffffff"]
|
8
|
+
|
9
|
+
def self.default
|
10
|
+
new DEFAULT.map { |hex| Color::RGB.from_html(hex) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def scores(histogram_scores)
|
14
|
+
scores = map do |palette_color|
|
15
|
+
score = 0
|
16
|
+
|
17
|
+
histogram_scores.each_with_index do |item, index|
|
18
|
+
color_score, color = *item
|
19
|
+
|
20
|
+
color = color.to_hsl.tap { |c| c.s = 0.05 + c.s * (4 - c.l * 2.5) }.to_rgb
|
21
|
+
|
22
|
+
if (distance = Metrics.distance(palette_color, color)) < 0.275
|
23
|
+
distance_penalty = (1 - distance) ** 4
|
24
|
+
score += color_score * distance_penalty
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
[score, palette_color]
|
29
|
+
end
|
30
|
+
|
31
|
+
scores.reject { |score, color| score <= 0.05 }.
|
32
|
+
sort_by { |score, color| score }.
|
33
|
+
reverse
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
Binary file
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require File.expand_path("../test_helper", __FILE__)
|
2
|
+
|
3
|
+
class HistogramTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@colors = 7
|
6
|
+
@histogram = Histogram.new("test/fixtures/skydiver.jpg", @colors)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_color_count_is_correct
|
10
|
+
assert_equal @colors, @histogram.colors.size
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path("../test_helper", __FILE__)
|
2
|
+
|
3
|
+
class MetricsTest < Test::Unit::TestCase
|
4
|
+
def test_no_distance_between_identical_colors
|
5
|
+
color = Color::RGB.new(123, 45, 67)
|
6
|
+
assert_equal 0, Metrics.distance(color, color)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_maximum_similarity_between_identical_colors
|
10
|
+
color = Color::RGB.new(123, 45, 67)
|
11
|
+
assert_equal 1, Metrics.similarity(color, color)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path("../test_helper", __FILE__)
|
2
|
+
|
3
|
+
class PaletteTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@histogram = Histogram.new("test/fixtures/skydiver.jpg")
|
6
|
+
@palette = Palette.default
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_skydiver_photo_is_mostly_blue
|
10
|
+
score, color = @palette.scores(@histogram.scores).first
|
11
|
+
assert_equal Color::RGB.from_html('0099cc'), color
|
12
|
+
end
|
13
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: colorscore
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Milo Winningham
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-08-25 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: color
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rake
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
description: Finds the dominant colors in an image and scores them against a user-defined palette, using the CIE2000 Delta E formula.
|
49
|
+
email:
|
50
|
+
- milo@winningham.net
|
51
|
+
executables: []
|
52
|
+
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
extra_rdoc_files: []
|
56
|
+
|
57
|
+
files:
|
58
|
+
- .gitignore
|
59
|
+
- Gemfile
|
60
|
+
- Rakefile
|
61
|
+
- colorscore.gemspec
|
62
|
+
- lib/colorscore.rb
|
63
|
+
- lib/colorscore/histogram.rb
|
64
|
+
- lib/colorscore/metrics.rb
|
65
|
+
- lib/colorscore/palette.rb
|
66
|
+
- lib/colorscore/version.rb
|
67
|
+
- test/fixtures/skydiver.jpg
|
68
|
+
- test/histogram_test.rb
|
69
|
+
- test/metrics_test.rb
|
70
|
+
- test/palette_test.rb
|
71
|
+
- test/test_helper.rb
|
72
|
+
homepage:
|
73
|
+
licenses: []
|
74
|
+
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
hash: 3
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
version: "0"
|
98
|
+
requirements: []
|
99
|
+
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 1.8.6
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: Finds the dominant colors in an image.
|
105
|
+
test_files:
|
106
|
+
- test/fixtures/skydiver.jpg
|
107
|
+
- test/histogram_test.rb
|
108
|
+
- test/metrics_test.rb
|
109
|
+
- test/palette_test.rb
|
110
|
+
- test/test_helper.rb
|