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.

@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in colorscore.gemspec
4
+ gemspec
@@ -0,0 +1,7 @@
1
+ require "rake/testtask"
2
+ require "bundler/gem_tasks"
3
+
4
+ task :default => :test
5
+ Rake::TestTask.new do |t|
6
+ t.test_files = FileList["test/test_helper.rb", "test/*_test.rb"]
7
+ end
@@ -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
@@ -0,0 +1,6 @@
1
+ require "color"
2
+
3
+ require "colorscore/histogram"
4
+ require "colorscore/metrics"
5
+ require "colorscore/palette"
6
+ require "colorscore/version"
@@ -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
@@ -0,0 +1,3 @@
1
+ module Colorscore
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ require "test/unit"
2
+ require "colorscore"
3
+ include Colorscore
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