coloractor 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: