jnylen-colorscore 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1aa628a12c958769b9f8e4dfa9fe4f8b436e42e8
4
+ data.tar.gz: 6bfcd87069d5461451e08bc88f05507d9004fc36
5
+ SHA512:
6
+ metadata.gz: 2b26dfc2e43aa85dea7693f4a4c88ea6b3f125912a9be0a484d3c99445937fa529da2ade54ced4a86caffd4aff1d8c3a0c1ea92d8a73f6b990f017843de8bb5f
7
+ data.tar.gz: dea93b215d1eaf5d6e271dfa158b420c0eb7cee7d738b541a2da8295060718bed57ce7454e088c35dfb5597e5f8ad8e3599ba0233e9fdfbcd707b044adb9bee8
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'test-unit'
4
+
5
+ # Specify your gem's dependencies in colorscore.gemspec
6
+ gemspec
data/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # Colorscore
2
+
3
+ Colorscore is a simple library that uses ImageMagick to quantize an image and find its representative colors. It can also score those colors against a palette using the CIE2000 Delta E formula. This could be used to index images for a "search by color" feature.
4
+
5
+ ## Requirements
6
+
7
+ * ImageMagick 6.5+
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ include Colorscore
13
+ histogram = Histogram.new('test/fixtures/skydiver.jpg')
14
+
15
+ # This image is 78.8% #7a9ab5:
16
+ histogram.scores.first # => [0.7884625, RGB [#7a9ab5]]
17
+
18
+ # This image is closest to pure blue:
19
+ palette = Palette.from_hex(['ff0000', '00ff00', '0000ff'])
20
+ scores = palette.scores(histogram.scores, 1)
21
+ scores.first # => [0.16493763694876, RGB [#0000ff]]
22
+ ```
data/Rakefile ADDED
@@ -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,21 @@
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 = "jnylen-colorscore"
7
+ s.version = Colorscore::VERSION
8
+ s.authors = ["Joakim Nylén", "Milo Winningham"]
9
+ s.email = ["me@jnylen.nu"]
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
+ s.add_development_dependency "test-unit"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
@@ -0,0 +1,43 @@
1
+ require "shellwords"
2
+
3
+ module Colorscore
4
+ class Histogram
5
+ def initialize(image_path, options = {})
6
+ params = [
7
+ '-resize 400x400',
8
+ '-format %c',
9
+ "-dither #{options.fetch(:dither) { 'None' }}"
10
+ "-quantize #{options.fetch(:quantize) { 'YIQ' }}",
11
+ "-colors #{options.fetch(:colors) { 16 }.to_i}",
12
+ "-depth #{options.fetch(:depth) { 8 }.to_i}",
13
+ '-alpha deactivate '
14
+ ]
15
+
16
+ #params.unshift(options[:resize]) if options[:resize]
17
+
18
+ output = `convert #{image_path.shellescape} #{ params.join(' ') } histogram:info:-`
19
+ @lines = output.lines.map(&:strip).reject(&:empty?).
20
+ sort_by { |l| l[/(\d+):/, 1].to_i }
21
+ end
22
+
23
+ # Returns an array of colors in descending order of occurances.
24
+ def hex_colors
25
+ hex_values = @lines.map { |line| line[/#([0-9A-F]{6}) /, 1] }.compact
26
+ hex_values.map { |hex| Color::RGB.from_html(hex) }
27
+ end
28
+
29
+ def rgb_colors
30
+ @lines.map { |line| line[/ \(([0-9, ]+)\) /, 1].split(',').map(&:strip).take(3).join(',') }.compact
31
+ end
32
+
33
+ def color_counts
34
+ @lines.map { |line| line.split(':')[0].to_i }
35
+ end
36
+
37
+ def scores
38
+ total = color_counts.inject(:+).to_f
39
+ scores = color_counts.map { |count| count / total }
40
+ scores.zip(colors)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,135 @@
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
+ h1p = ([b1, a1p] == [0.0, 0.0]) ? 0.0 : degrees(Math.atan2(b1,a1p))
33
+ h1p += 360 if h1p < 0
34
+
35
+ h2p = ([b2, a2p] == [0.0, 0.0]) ? 0.0 : degrees(Math.atan2(b2,a2p))
36
+ h2p += 360 if h2p < 0
37
+
38
+ if (h1p - h2p).abs > 180
39
+ avg_hp = (h1p + h2p + 360) / 2.0
40
+ else
41
+ avg_hp = (h1p + h2p) / 2.0
42
+ end
43
+
44
+ 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))
45
+
46
+ diff_h2p_h1p = h2p - h1p
47
+ if diff_h2p_h1p.abs <= 180
48
+ delta_hp = diff_h2p_h1p
49
+ elsif diff_h2p_h1p.abs > 180 && h2p <= h1p
50
+ delta_hp = diff_h2p_h1p + 360
51
+ else
52
+ delta_hp = diff_h2p_h1p - 360
53
+ end
54
+
55
+ delta_lp = l2 - l1
56
+ delta_cp = c2p - c1p
57
+ delta_hp = 2 * Math.sqrt(c2p * c1p) * Math.sin(radians(delta_hp) / 2.0)
58
+
59
+ s_l = 1 + ((0.015 * ((avg_lp - 50) ** 2)) / Math.sqrt(20 + ((avg_lp - 50) ** 2.0)))
60
+ s_c = 1 + 0.045 * avg_c1p_c2p
61
+ s_h = 1 + 0.015 * avg_c1p_c2p * t
62
+
63
+ delta_ro = 30 * Math.exp(-((((avg_hp - 275) / 25) ** 2.0)))
64
+ r_c = Math.sqrt(((avg_c1p_c2p ** 7.0)) / ((avg_c1p_c2p ** 7.0) + (25.0 ** 7.0)));
65
+ r_t = -2 * r_c * Math.sin(2 * radians(delta_ro))
66
+
67
+ 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)))
68
+ end
69
+
70
+ def self.rgb_to_xyz(color)
71
+ color = color.to_rgb
72
+ r, g, b = color.r, color.g, color.b
73
+
74
+ # assuming sRGB (D65)
75
+ r = (r <= 0.04045) ? r/12.92 : ((r+0.055)/1.055) ** 2.4
76
+ g = (g <= 0.04045) ? g/12.92 : ((g+0.055)/1.055) ** 2.4
77
+ b = (b <= 0.04045) ? b/12.92 : ((b+0.055)/1.055) ** 2.4
78
+
79
+ r *= 100
80
+ g *= 100
81
+ b *= 100
82
+
83
+ x = 0.412453*r + 0.357580*g + 0.180423*b
84
+ y = 0.212671*r + 0.715160*g + 0.072169*b
85
+ z = 0.019334*r + 0.119193*g + 0.950227*b
86
+
87
+ [x, y, z]
88
+ end
89
+
90
+ def self.xyz_to_lab(x, y, z)
91
+ x /= 95.047
92
+ y /= 100.000
93
+ z /= 108.883
94
+
95
+ if x > 0.008856
96
+ x = x ** (1.0/3)
97
+ else
98
+ x = (7.787 * x) + (16.0 / 116)
99
+ end
100
+
101
+ if y > 0.008856
102
+ y = y ** (1.0/3)
103
+ else
104
+ y = (7.787 * y) + (16.0 / 116)
105
+ end
106
+
107
+ if z > 0.008856
108
+ z = z ** (1.0/3)
109
+ else
110
+ z = (7.787 * z) + (16.0 / 116)
111
+ end
112
+
113
+ l = (116.0 * y) - 16.0
114
+ a = 500.0 * (x - y)
115
+ b = 200.0 * (y - z)
116
+
117
+ [l, a, b]
118
+ end
119
+
120
+ def self.scale(number, from_range, to_range=0..1, clamp=true)
121
+ if clamp && number <= from_range.begin
122
+ position = 0
123
+ elsif clamp && number >= from_range.end
124
+ position = 1
125
+ else
126
+ position = (number - from_range.begin).to_f / (from_range.end - from_range.begin)
127
+ end
128
+
129
+ position * (to_range.end - to_range.begin) + to_range.begin
130
+ end
131
+
132
+ def self.radians(degrees); degrees * Math::PI / 180; end
133
+ def self.degrees(radians); radians * 180 / Math::PI; end
134
+ end
135
+ end
@@ -0,0 +1,68 @@
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
+ from_hex DEFAULT
11
+ end
12
+
13
+ def self.from_hex(hex_values)
14
+ new hex_values.map { |hex| Color::RGB.from_html(hex) }
15
+ end
16
+
17
+ def scores(histogram_scores, distance_threshold=0.275)
18
+ scores = map do |palette_color|
19
+ score = 0
20
+
21
+ histogram_scores.each_with_index do |item, index|
22
+ color_score, color = *item
23
+
24
+ color = color.to_hsl.tap { |c| c.s = 0.05 + c.s * (4 - c.l * 2.5) }.to_rgb
25
+
26
+ if (distance = Metrics.distance(palette_color, color)) < distance_threshold
27
+ distance_penalty = (1 - distance) ** 4
28
+ score += color_score * distance_penalty
29
+ end
30
+ end
31
+
32
+ [score, palette_color]
33
+ end
34
+
35
+ scores.reject { |score, color| score <= 0.05 }.
36
+ sort_by { |score, color| score }.
37
+ reverse
38
+ end
39
+
40
+ # From a large palette, collects scores and reduces them based on
41
+ # the distance threshold (which can be varied to find a specific number of colors)
42
+ def collect_scores(distance_threshold=0.275, min_colors=3, max_colors=7)
43
+ def generate_scores(threshold)
44
+ @res = [[1,self[0]]]
45
+ @pivot = self[0]
46
+ self.each do |color|
47
+ if (Metrics.distance(@pivot, color)) > threshold
48
+ @res << [1,color]
49
+ @pivot = color
50
+ else
51
+ @res.last[0] +=1
52
+ end
53
+ end
54
+ @res
55
+ end
56
+
57
+ @results = generate_scores(distance_threshold)
58
+
59
+ if @results.length <= min_colors && distance_threshold > 0.05
60
+ self.collect_scores((distance_threshold-0.05))
61
+ elsif @results.length > max_colors && distance_threshold < 0.5
62
+ self.collect_scores((distance_threshold+0.05))
63
+ else
64
+ return @results
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module Colorscore
2
+ VERSION = "0.1.0"
3
+ end
data/lib/colorscore.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "color"
2
+
3
+ require "colorscore/histogram"
4
+ require "colorscore/metrics"
5
+ require "colorscore/palette"
6
+ require "colorscore/version"
Binary file
Binary file
@@ -0,0 +1,14 @@
1
+ require File.expand_path("../test_helper", __FILE__)
2
+
3
+ class HistogramTest < Test::Unit::TestCase
4
+ def test_color_count_is_correct
5
+ colors = 7
6
+ histogram = Histogram.new("test/fixtures/skydiver.jpg", colors)
7
+ assert_operator histogram.colors.size, :<=, colors
8
+ end
9
+
10
+ def test_transparency_is_ignored
11
+ histogram = Histogram.new("test/fixtures/transparency.png")
12
+ assert_equal Color::RGB.from_html('0000ff'), histogram.colors.first
13
+ end
14
+ 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,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jnylen-colorscore
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joakim Nylén
8
+ - Milo Winningham
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-08-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: color
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rake
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: test-unit
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ description: Finds the dominant colors in an image and scores them against a user-defined
57
+ palette, using the CIE2000 Delta E formula.
58
+ email:
59
+ - me@jnylen.nu
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - Gemfile
66
+ - README.md
67
+ - Rakefile
68
+ - colorscore.gemspec
69
+ - lib/colorscore.rb
70
+ - lib/colorscore/histogram.rb
71
+ - lib/colorscore/metrics.rb
72
+ - lib/colorscore/palette.rb
73
+ - lib/colorscore/version.rb
74
+ - test/fixtures/skydiver.jpg
75
+ - test/fixtures/transparency.png
76
+ - test/histogram_test.rb
77
+ - test/metrics_test.rb
78
+ - test/palette_test.rb
79
+ - test/test_helper.rb
80
+ homepage:
81
+ licenses: []
82
+ metadata: {}
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 2.5.1
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Finds the dominant colors in an image.
103
+ test_files: []