coloractor 0.0.1

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: c09735acda7beea8d3b3f86be079ce3d3043e0e4
4
+ data.tar.gz: 9c421a2676a5e1dab33fb13ce68e84c8c2dc18d4
5
+ SHA512:
6
+ metadata.gz: 7fe619616b81e916ccf6665127b7b790f07e9768f83dbc32ed9f1b8535dc6f00cb5295f64a9f5560549e603857ddd1cbd54e28898a110e61cab854d591d1b057
7
+ data.tar.gz: 2fd80ac41bba4ccdda997488b49b3da8e1ca39bfd504b08d663ea08a1956594591300178a079b52e5fe491c77a91bf55f0affe5ef9539ec03a1ee6ee7849df78
@@ -0,0 +1,181 @@
1
+ require 'color'
2
+ require 'mini_magick'
3
+
4
+ class Coloractor::Palette
5
+
6
+ N_QUANTIZED = 100
7
+ MIN_DISTANCE = 10.0
8
+ MIN_SATURATION = 0.05
9
+ MIN_PROMINENCE = 0.01
10
+ MAX_COLORS = 5
11
+
12
+ def self.build_from_file(filename)
13
+ image = open_image(filename)
14
+
15
+ unless image_mode_is_rgb?(image)
16
+ image = convert_image_mode_to_rgb(image)
17
+ end
18
+
19
+ image = trim_image(image)
20
+ image = resize_image(image)
21
+ image = reduce_colors_in_image(image, N_QUANTIZED)
22
+
23
+ canonical_colors = { '#FFFFFFFF' => '#FFFFFFFF', '#000000FF' => '#000000FF' }
24
+ aggregated_colors = { '#FFFFFFFF' => 0, '#000000FF' => 0 }
25
+
26
+ grouped_pixels = get_pixels_from_image(image)
27
+ pixels_count = grouped_pixels.values.reduce(:+)
28
+
29
+ sorted_pixels = grouped_pixels.sort_by{ |color, count| -count }
30
+ sorted_pixels.each do |(color, count)|
31
+ if aggregated_colors.include?(color)
32
+ aggregated_colors[color] += count
33
+ else
34
+ closest_color = find_closest_color(color, aggregated_colors)
35
+
36
+ if closest_color && distance(color, closest_color) < MIN_DISTANCE
37
+ aggregated_colors[closest_color] += count
38
+ canonical_colors[color] = closest_color
39
+ else
40
+ aggregated_colors[color] = count
41
+ canonical_colors[color] = color
42
+ end
43
+ end
44
+ end
45
+
46
+ sorted_aggregated_colors = aggregated_colors.sort_by{ |color, count| -count }
47
+ background_color = detect_background_color(image, sorted_aggregated_colors, canonical_colors)
48
+
49
+ colors = sorted_aggregated_colors.map{ |(color, count)| color }.select{ |c| background_color.nil? || c != background_color }
50
+
51
+ saturated_colors = colors.select{ |color| calculate_saturation(color) >= MIN_SATURATION }
52
+ if background_color && calculate_opacity(background_color).zero?
53
+ background_color = nil
54
+ end
55
+
56
+ if saturated_colors.size > 0
57
+ colors = saturated_colors
58
+ else
59
+ colors = [ colors.first ]
60
+ end
61
+
62
+ palette_colors = []
63
+ colors.each do |color|
64
+ if calculate_prominence(color, sorted_aggregated_colors, pixels_count) >= calculate_prominence(colors.first, sorted_aggregated_colors, pixels_count) * MIN_PROMINENCE
65
+ palette_colors << color
66
+ end
67
+
68
+ break if palette_colors.size >= MAX_COLORS
69
+ end
70
+
71
+ new(palette_colors.map{ |c| c[0..6] }, background_color ? background_color[0..6] : nil)
72
+ end
73
+
74
+ attr_reader :colors, :background_color
75
+
76
+ def initialize(colors, background_color)
77
+ @colors = colors
78
+ @background_color = background_color
79
+ end
80
+
81
+ private
82
+
83
+ def self.open_image(filename)
84
+ MiniMagick::Image.open(filename)
85
+ end
86
+
87
+ def self.image_mode_is_rgb?(image)
88
+ image['colorspace'] =~ /rgb/i
89
+ end
90
+
91
+ def self.convert_image_mode_to_rgb(image)
92
+ image.tap{ |i| i.colorspace "sRGB" }
93
+ end
94
+
95
+ def self.trim_image(image)
96
+ image.tap(&:trim)
97
+ end
98
+
99
+ def self.resize_image(image)
100
+ image.tap{ |i| i.resize '600x600\>' }
101
+ end
102
+
103
+ def self.reduce_colors_in_image(image, max_colors)
104
+ image.tap{ |i| i.combine_options{ |o| o.dither 'None'; o.colors max_colors } }
105
+ end
106
+
107
+ def self.get_pixels_from_image(image)
108
+ output = `convert #{ image.path } -dither None -colors 100 -define histogram:unique-colors=true -format %c histogram:info:`
109
+ grouped_pixels = Hash[ *output.scan(/(\d+?):.+(#[0-9a-fA-F]{6,8})/).map{ |a, b| [b, a.to_i] }.flatten ]
110
+ end
111
+
112
+ def self.find_closest_color(color, colors)
113
+ color, _ = colors.min_by do |(c, _)|
114
+ distance(c, color)
115
+ end
116
+
117
+ color
118
+ end
119
+
120
+ def self.distance(color1, color2)
121
+ color1_transparency = calculate_opacity(color1)
122
+ color2_transparency = calculate_opacity(color2)
123
+ Color::RGB.new.delta_e94(Color::RGB.by_hex(color1[1..6]).to_lab, Color::RGB.by_hex(color2[1..6]).to_lab) + (color1_transparency - color2_transparency).abs
124
+ end
125
+
126
+ def self.calculate_opacity(color)
127
+ color.size > 7 ? color[7..8].to_i(16) : 255
128
+ end
129
+
130
+ def self.detect_background_color(image, colors, canonical_colors)
131
+ # We want to remove background color based on key points only. Some logos might be solid color based and they are treated as background then.
132
+ # TODO: remove commented code if it is not needed anymore
133
+ # if colors.first && calculate_prominence(colors.first.first, colors, pixels.size) >= BACKGROUND_PROMINENCE
134
+ # return colors.first.first
135
+ # end
136
+
137
+ width, height = get_image_size(image)
138
+
139
+ points = [
140
+ [0, 0],
141
+ [0, height / 2],
142
+ [0, height - 1],
143
+ [width - 1, height - 1],
144
+ [width - 1, height / 2],
145
+ [width - 1, 0],
146
+ [width / 2, 0]
147
+ ]
148
+
149
+ edge_pixels = points.map{ |(left, top)| get_image_pixel_color(image, left, top) }
150
+ grouped_edge_pixels = Hash[ *edge_pixels.group_by{|i| i}.map{|k,v| [k, v.count] }.flatten ]
151
+ sorted_pixels = grouped_edge_pixels.sort_by{ |color, count| count }.reverse
152
+ majority_color, majority_count = sorted_pixels.first
153
+ if majority_count >= 3
154
+ canonical_colors[majority_color]
155
+ else
156
+ nil
157
+ end
158
+ end
159
+
160
+ def self.get_image_size(image)
161
+ image['dimensions']
162
+ end
163
+
164
+ def self.get_image_pixel_color(image, left, top)
165
+ image.run_command("convert", "#{image.path}[1x1+#{left.to_i}+#{top.to_i}]", 'txt:').split("\n").each do |line|
166
+ return $1 if /^0,0:.*(#[0-9a-fA-F]+)/.match(line)
167
+ end
168
+ nil
169
+ end
170
+
171
+ def self.calculate_prominence(color, colors, pixels_count)
172
+ colors = Hash[ *colors.flatten ] if colors.is_a?(Array)
173
+
174
+ colors[color] / pixels_count.to_f
175
+ end
176
+
177
+ def self.calculate_saturation(color)
178
+ Color::RGB.by_hex(color[1..6]).to_hsl.saturation / 100
179
+ end
180
+
181
+ end
data/lib/coloractor.rb ADDED
@@ -0,0 +1,25 @@
1
+ class Coloractor
2
+
3
+ def self.extract_colors(filename)
4
+ new(filename).tap do |instance|
5
+ instance.extract_colors
6
+ end
7
+ end
8
+
9
+ attr_reader :dominant_colors, :background_color
10
+
11
+ def initialize(filename)
12
+ @filename = filename
13
+ @dominant_colors = []
14
+ end
15
+
16
+ def extract_colors
17
+ palette = Coloractor::Palette.build_from_file(@filename)
18
+
19
+ @dominant_colors = palette.colors
20
+ @background_color = palette.background_color
21
+ end
22
+
23
+ end
24
+
25
+ require 'coloractor/palette'
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: coloractor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Maxim Gladkov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mini_magick
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: color
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: A simple ruby wrapper around colorific Python script.
56
+ email: contact@maximgladkov.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/coloractor.rb
62
+ - lib/coloractor/palette.rb
63
+ homepage: http://rubygems.org/gems/colorator
64
+ licenses:
65
+ - MIT
66
+ metadata: {}
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 2.2.2
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: Dominant colors extractor from an image file
87
+ test_files: []
88
+ has_rdoc: