gauguin 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +119 -0
- data/Rakefile +13 -0
- data/gauguin.gemspec +30 -0
- data/lib/gauguin/color.rb +76 -0
- data/lib/gauguin/color_space/lab_vector.rb +6 -0
- data/lib/gauguin/color_space/rgb_vector.rb +36 -0
- data/lib/gauguin/color_space/xyz_vector.rb +33 -0
- data/lib/gauguin/color_space.rb +4 -0
- data/lib/gauguin/colors_clusterer.rb +63 -0
- data/lib/gauguin/colors_limiter.rb +14 -0
- data/lib/gauguin/colors_retriever.rb +33 -0
- data/lib/gauguin/image.rb +55 -0
- data/lib/gauguin/image_recolorer.rb +29 -0
- data/lib/gauguin/image_repository.rb +7 -0
- data/lib/gauguin/noise_reducer.rb +26 -0
- data/lib/gauguin/painting.rb +29 -0
- data/lib/gauguin/palette_serializer.rb +22 -0
- data/lib/gauguin/version.rb +3 -0
- data/lib/gauguin.rb +43 -0
- data/spec/integration/painting_spec.rb +79 -0
- data/spec/integration/samples_spec.rb +43 -0
- data/spec/lib/gauguin/color_space/rgb_vector_spec.rb +15 -0
- data/spec/lib/gauguin/color_space/xyz_vector_spec.rb +15 -0
- data/spec/lib/gauguin/color_spec.rb +125 -0
- data/spec/lib/gauguin/colors_clusterer_spec.rb +158 -0
- data/spec/lib/gauguin/colors_limiter_spec.rb +27 -0
- data/spec/lib/gauguin/colors_retriever_spec.rb +85 -0
- data/spec/lib/gauguin/image_recolorer_spec.rb +94 -0
- data/spec/lib/gauguin/image_repository_spec.rb +15 -0
- data/spec/lib/gauguin/image_spec.rb +90 -0
- data/spec/lib/gauguin/noise_reducer_spec.rb +51 -0
- data/spec/lib/gauguin/painting_spec.rb +55 -0
- data/spec/lib/gauguin/palette_serializer_spec.rb +24 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/support/pictures/10_colors.png +0 -0
- data/spec/support/pictures/12_colors.png +0 -0
- data/spec/support/pictures/gauguin.png +0 -0
- data/spec/support/pictures/gray_and_black.png +0 -0
- data/spec/support/pictures/not_unique_colors.png +0 -0
- data/spec/support/pictures/samples/sample1.png +0 -0
- data/spec/support/pictures/samples/sample10.png +0 -0
- data/spec/support/pictures/samples/sample11.png +0 -0
- data/spec/support/pictures/samples/sample2.png +0 -0
- data/spec/support/pictures/samples/sample3.png +0 -0
- data/spec/support/pictures/samples/sample4.png +0 -0
- data/spec/support/pictures/samples/sample5.png +0 -0
- data/spec/support/pictures/samples/sample6.png +0 -0
- data/spec/support/pictures/samples/sample7.png +0 -0
- data/spec/support/pictures/samples/sample8.png +0 -0
- data/spec/support/pictures/samples/sample9.png +0 -0
- data/spec/support/pictures/too_many_colors.png +0 -0
- data/spec/support/pictures/transparent_background.png +0 -0
- data/spec/support/pictures/unique_colors.png +0 -0
- metadata +251 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6a07d005423451283af94f40f2883014463654d6
|
4
|
+
data.tar.gz: 94c047bc3adbdfd5a9c312133b7650ad2558b379
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0b8d86912f76d6ec17cf14e400c346a89559b267d8c345d8697760fae9bbc1158c6cb50130c26f7f566b6b3e866885b407fd4316c307f088df2a093b82ef60fd
|
7
|
+
data.tar.gz: 3bf13e35ba5fcda29a71cf7b9242802da1bdeb0aaadf686d8af988264269b3e2095db8ac93a23c112b08c4862cc5b85c317e34f825a376f84e7209edc7f43404
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Lunar Logic Polska http://lunarlogicpolska.com
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/LunarLogic/gauguin.svg?branch=master)](https://travis-ci.org/LunarLogic/gauguin)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/LunarLogic/gauguin/badges/gpa.svg)](https://codeclimate.com/github/LunarLogic/gauguin)
|
3
|
+
[![Test Coverage](https://codeclimate.com/github/LunarLogic/gauguin/badges/coverage.svg)](https://codeclimate.com/github/LunarLogic/gauguin)
|
4
|
+
|
5
|
+
<img src="http://gauguin.lunarlogic.io/assets/gauguin-b7a7737e8ede819b98df9d05f7df020a.png" alt="Guard Icon" align="left" />
|
6
|
+
# Gauguin
|
7
|
+
|
8
|
+
Retrieves palette of main colors, merging similar colors using [Lab color space](http://en.wikipedia.org/wiki/Lab_color_space).
|
9
|
+
|
10
|
+
## Why not just use `RMagick`?
|
11
|
+
|
12
|
+
How many colors do you recognize on the image below?
|
13
|
+
|
14
|
+
![Black and white image](spec/support/pictures/gray_and_black.png)
|
15
|
+
|
16
|
+
Many people would say `2`, but actually there are `1942`.
|
17
|
+
|
18
|
+
It's because of the fact that to make image more smooth, borders of the figure are not pure black but consist of many gray scale colors.
|
19
|
+
|
20
|
+
It's common that images includes very similar colors, so when you want to get useful color palette, you would need to process color histogram you get from `RMagick` yourself.
|
21
|
+
|
22
|
+
This gem was created to do this for you.
|
23
|
+
|
24
|
+
## Sample app
|
25
|
+
|
26
|
+
Sample application available here: http://gauguin.lunarlogic.io
|
27
|
+
|
28
|
+
## Requirements
|
29
|
+
|
30
|
+
Gem depends on `RMagick` which requires `ImageMagick` to be installed.
|
31
|
+
|
32
|
+
### Ubuntu
|
33
|
+
|
34
|
+
$ sudo apt-get install imagemagick
|
35
|
+
|
36
|
+
### OSX
|
37
|
+
|
38
|
+
$ brew install imagemagick
|
39
|
+
|
40
|
+
## Installation
|
41
|
+
|
42
|
+
Add this line to your application's Gemfile:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
gem 'gauguin'
|
46
|
+
```
|
47
|
+
|
48
|
+
And then execute:
|
49
|
+
|
50
|
+
$ bundle
|
51
|
+
|
52
|
+
Or install it yourself as:
|
53
|
+
|
54
|
+
$ gem install gauguin
|
55
|
+
|
56
|
+
## Usage
|
57
|
+
|
58
|
+
#### Palette
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
palette = Gauguin::Painting.new("path/to/image.png").palette
|
62
|
+
```
|
63
|
+
|
64
|
+
Result for image above would be:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
{
|
68
|
+
rgb(204, 204, 204)[0.5900935269505287] => [
|
69
|
+
rgb(77, 77, 77)[7.383706620723603e-05],
|
70
|
+
rgb(85, 85, 85)[0.00012306177701206005],
|
71
|
+
# ...
|
72
|
+
rgb(219, 220, 219)[1.2306177701206005e-05],
|
73
|
+
rgb(220, 220, 220)[7.383706620723603e-05]
|
74
|
+
],
|
75
|
+
rgb(0, 0, 0)[0.40990647304947003] => [
|
76
|
+
rgb(0, 0, 0)[0.40990647304947003],
|
77
|
+
rgb(1, 1, 1)[0.007912872261875462],
|
78
|
+
# ...
|
79
|
+
rgb(64, 64, 64)[6.153088850603002e-05],
|
80
|
+
rgb(66, 66, 66)[6.153088850603002e-05]
|
81
|
+
]
|
82
|
+
}
|
83
|
+
```
|
84
|
+
|
85
|
+
Where keys are instances of `Gauguin::Color` class and values are array of instances of `Gauguin::Color` class.
|
86
|
+
|
87
|
+
#### Recolor
|
88
|
+
|
89
|
+
There is also recolor feature - you can pass original image and the calculated palette and return new image, colored only with the main colours from the palette.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
painting.recolor(palette, 'path/where/recolored/file/will/be/placed')
|
93
|
+
```
|
94
|
+
|
95
|
+
## Custom configuration
|
96
|
+
|
97
|
+
There are `4` parameters that you can configure:
|
98
|
+
|
99
|
+
- `max_colors_count` (default value is `10`) - maximum number of colors that a palette will include
|
100
|
+
- `colors_limit` (default value is `10000`) - maximum number of colors that will be considered while calculating a palette - if image has too many colors it is not efficient to calculate grouping for all of them, so only `colors_limit` of colors of the largest percentage are used
|
101
|
+
- `min_percentage_sum` (default value is `0.981`) - parameter used while calculating which colors should be ignored. Colors are sorted by percentage in descending order, then colors which percentages sums to `min_percentage_sum` are taken into consideration
|
102
|
+
- `color_similarity_threshold` (default value is `25`) - maximum distance in [Lab color space](http://en.wikipedia.org/wiki/Lab_color_space) to consider two colors as the same while grouping
|
103
|
+
|
104
|
+
To configure any of above options you can use configuration block.
|
105
|
+
For example changing `max_colors_count` would look like this:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
Gauguin.configuration do |config|
|
109
|
+
config.max_colors_count = 7
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
## Contributing
|
114
|
+
|
115
|
+
1. Fork it ( https://github.com/LunarLogic/gauguin/fork )
|
116
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
117
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
118
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
119
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/gauguin.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'gauguin/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "gauguin"
|
8
|
+
spec.version = Gauguin::VERSION
|
9
|
+
spec.authors = ["Ania Slimak"]
|
10
|
+
spec.email = ["anna.slimak@lunarlogic.io"]
|
11
|
+
spec.summary = %q{Tool for retrieving main colors from the image.}
|
12
|
+
spec.description = %q{Retrieves palette of main colors, merging similar colors using Lab color space.}
|
13
|
+
spec.homepage = "https://github.com/LunarLogic/gauguin"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "rmagick"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
25
|
+
spec.add_development_dependency "rspec"
|
26
|
+
spec.add_development_dependency "simplecov"
|
27
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
28
|
+
spec.add_development_dependency "guard-rspec"
|
29
|
+
spec.add_development_dependency "pry"
|
30
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Gauguin
|
2
|
+
class Color
|
3
|
+
attr_accessor :red, :green, :blue, :percentage, :transparent
|
4
|
+
|
5
|
+
def initialize(red, green, blue, percentage = 1, transparent = false)
|
6
|
+
self.red = red
|
7
|
+
self.green = green
|
8
|
+
self.blue = blue
|
9
|
+
self.percentage = percentage
|
10
|
+
self.transparent = transparent
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
self.class == other.class && self.to_key == other.to_key
|
15
|
+
end
|
16
|
+
|
17
|
+
alias eql? ==
|
18
|
+
|
19
|
+
def hash
|
20
|
+
self.to_key.hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def similar?(other_color)
|
24
|
+
self.transparent == other_color.transparent &&
|
25
|
+
self.distance(other_color) < Gauguin.configuration.color_similarity_threshold
|
26
|
+
end
|
27
|
+
|
28
|
+
def distance(other_color)
|
29
|
+
(self.to_lab - other_color.to_lab).r
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_lab
|
33
|
+
rgb_vector = self.to_vector
|
34
|
+
xyz_vector = rgb_vector.to_xyz
|
35
|
+
xyz_vector.to_lab
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_vector
|
39
|
+
ColorSpace::RgbVector[*to_rgb]
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_rgb
|
43
|
+
[self.red, self.green, self.blue]
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_a
|
47
|
+
to_rgb + [self.percentage, self.transparent]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.from_a(array)
|
51
|
+
red, green, blue, percentage, transparent = array
|
52
|
+
Color.new(red, green, blue, percentage, transparent)
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_key
|
56
|
+
to_rgb + [self.transparent]
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
"rgb(#{self.red}, #{self.green}, #{self.blue})"
|
61
|
+
end
|
62
|
+
|
63
|
+
def inspect
|
64
|
+
msg = "#{to_s}[#{percentage}]"
|
65
|
+
if transparent?
|
66
|
+
msg += "[transparent]"
|
67
|
+
end
|
68
|
+
msg
|
69
|
+
end
|
70
|
+
|
71
|
+
def transparent?
|
72
|
+
self.transparent
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Gauguin
|
2
|
+
module ColorSpace
|
3
|
+
class RgbVector < Vector
|
4
|
+
MAX_VAUE = 255.0
|
5
|
+
|
6
|
+
# Observer. = 2°, Illuminant = D65
|
7
|
+
RGB_TO_XYZ = Matrix[[0.4124, 0.2126, 0.0193],
|
8
|
+
[0.3576, 0.7152, 0.1192],
|
9
|
+
[0.1805, 0.0722, 0.9505]]
|
10
|
+
|
11
|
+
def pivot!
|
12
|
+
self.each.with_index do |component, i|
|
13
|
+
self[i] = pivot(component / MAX_VAUE)
|
14
|
+
end
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_xyz
|
19
|
+
self.pivot!
|
20
|
+
matrix = Matrix[self] * RGB_TO_XYZ
|
21
|
+
XyzVector[*matrix.row_vectors.first.to_a]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def pivot(component)
|
27
|
+
component = if component > 0.04045
|
28
|
+
((component + 0.055) / 1.055) ** 2.4
|
29
|
+
else
|
30
|
+
component / 12.92
|
31
|
+
end
|
32
|
+
component * 100.0
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Gauguin
|
2
|
+
module ColorSpace
|
3
|
+
class XyzVector < Vector
|
4
|
+
WHITE_REFERENCE = self[95.047, 100.000, 108.883]
|
5
|
+
|
6
|
+
EPSILON = 0.008856
|
7
|
+
KAPPA = 903.3
|
8
|
+
|
9
|
+
def to_lab
|
10
|
+
zipped = self.zip(XyzVector::WHITE_REFERENCE)
|
11
|
+
x, y, z = zipped.map do |component, white_component|
|
12
|
+
component / white_component
|
13
|
+
end
|
14
|
+
|
15
|
+
l = 116 * f(y) - 16
|
16
|
+
a = 500 * (f(x) - f(y))
|
17
|
+
b = 200 * (f(y) - f(z))
|
18
|
+
|
19
|
+
LabVector[l, a, b]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def f(x)
|
25
|
+
if x > EPSILON
|
26
|
+
x ** (1.0/3.0)
|
27
|
+
else
|
28
|
+
(KAPPA * x + 16.0) / 116.0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Gauguin
|
2
|
+
class ColorsClusterer
|
3
|
+
def call(colors)
|
4
|
+
clusters = {}
|
5
|
+
|
6
|
+
while !colors.empty?
|
7
|
+
pivot = colors.shift
|
8
|
+
group = [pivot]
|
9
|
+
|
10
|
+
colors, pivot, group = find_all_similar(colors, pivot, group)
|
11
|
+
|
12
|
+
clusters[pivot] = group
|
13
|
+
end
|
14
|
+
|
15
|
+
update_pivots_percentages(clusters)
|
16
|
+
|
17
|
+
clusters
|
18
|
+
end
|
19
|
+
|
20
|
+
def clusters(colors)
|
21
|
+
clusters = self.call(colors)
|
22
|
+
clusters = clusters.sort_by { |color, _| color.percentage }.reverse
|
23
|
+
Hash[clusters[0...Gauguin.configuration.max_colors_count]]
|
24
|
+
end
|
25
|
+
|
26
|
+
def reversed_clusters(clusters)
|
27
|
+
reversed_clusters = {}
|
28
|
+
|
29
|
+
clusters.each do |pivot, group|
|
30
|
+
group.each do |color|
|
31
|
+
reversed_clusters[color] = pivot
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
reversed_clusters
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def find_all_similar(colors, pivot, group)
|
41
|
+
loop do
|
42
|
+
similar_colors = colors.select { |c| c.similar?(pivot) }
|
43
|
+
break if similar_colors.empty?
|
44
|
+
|
45
|
+
group += similar_colors
|
46
|
+
colors -= similar_colors
|
47
|
+
|
48
|
+
pivot = group.sort_by(&:percentage).last
|
49
|
+
end
|
50
|
+
|
51
|
+
[colors, pivot, group]
|
52
|
+
end
|
53
|
+
|
54
|
+
def update_pivots_percentages(clusters)
|
55
|
+
clusters.each do |main_color, group|
|
56
|
+
percentage = group.inject(0) do |sum, color|
|
57
|
+
sum += color.percentage
|
58
|
+
end
|
59
|
+
main_color.percentage = percentage
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Gauguin
|
2
|
+
class ColorsLimiter
|
3
|
+
def call(colors)
|
4
|
+
colors_limit = Gauguin.configuration.colors_limit
|
5
|
+
|
6
|
+
if colors.count > colors_limit
|
7
|
+
colors = colors.sort_by { |key, group| key.percentage }.
|
8
|
+
reverse[0..colors_limit - 1]
|
9
|
+
end
|
10
|
+
|
11
|
+
colors
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Gauguin
|
2
|
+
class ColorsRetriever
|
3
|
+
def initialize(image)
|
4
|
+
@image = image
|
5
|
+
end
|
6
|
+
|
7
|
+
def colors
|
8
|
+
colors = {}
|
9
|
+
|
10
|
+
histogram = @image.color_histogram
|
11
|
+
image_size = @image.columns * @image.rows
|
12
|
+
|
13
|
+
histogram.each do |pixel, count|
|
14
|
+
image_pixel = @image.pixel(pixel)
|
15
|
+
|
16
|
+
red, green, blue = image_pixel.to_rgb
|
17
|
+
percentage = count.to_f / image_size
|
18
|
+
color = Gauguin::Color.new(red, green, blue, percentage,
|
19
|
+
image_pixel.transparent?)
|
20
|
+
|
21
|
+
# histogram can contain different magic pixels for
|
22
|
+
# the same colors with different opacity
|
23
|
+
if colors[color]
|
24
|
+
colors[color].percentage += color.percentage
|
25
|
+
else
|
26
|
+
colors[color] = color
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
colors.values
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rmagick'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Gauguin
|
5
|
+
class Image
|
6
|
+
extend Forwardable
|
7
|
+
attr_accessor :image
|
8
|
+
delegate [:color_histogram, :columns, :rows, :write] => :image
|
9
|
+
|
10
|
+
def initialize(path = nil)
|
11
|
+
return unless path
|
12
|
+
|
13
|
+
list = Magick::ImageList.new(path)
|
14
|
+
self.image = list.first
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.blank(columns, rows)
|
18
|
+
blank_image = Image.new
|
19
|
+
transparent_white = Magick::Pixel.new(255, 255, 255, Pixel::MAX_TRANSPARENCY)
|
20
|
+
blank_image.image = Magick::Image.new(columns, rows) do
|
21
|
+
self.background_color = transparent_white
|
22
|
+
end
|
23
|
+
blank_image
|
24
|
+
end
|
25
|
+
|
26
|
+
def pixel(magic_pixel)
|
27
|
+
Pixel.new(magic_pixel)
|
28
|
+
end
|
29
|
+
|
30
|
+
def pixel_color(row, column, *args)
|
31
|
+
magic_pixel = self.image.pixel_color(row, column, *args)
|
32
|
+
pixel(magic_pixel)
|
33
|
+
end
|
34
|
+
|
35
|
+
class Pixel
|
36
|
+
MAX_CHANNEL_VALUE = 257
|
37
|
+
MAX_TRANSPARENCY = 65535
|
38
|
+
|
39
|
+
def initialize(magic_pixel)
|
40
|
+
@magic_pixel = magic_pixel
|
41
|
+
end
|
42
|
+
|
43
|
+
def transparent?
|
44
|
+
@magic_pixel.opacity >= MAX_TRANSPARENCY
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_rgb
|
48
|
+
[:red, :green, :blue].map do |color|
|
49
|
+
@magic_pixel.send(color) / MAX_CHANNEL_VALUE
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Gauguin
|
2
|
+
class ImageRecolorer
|
3
|
+
def initialize(image)
|
4
|
+
@image = image.dup
|
5
|
+
end
|
6
|
+
|
7
|
+
def recolor(new_colors)
|
8
|
+
columns = @image.columns
|
9
|
+
rows = @image.rows
|
10
|
+
|
11
|
+
new_image = Image.blank(columns, rows)
|
12
|
+
|
13
|
+
(0...columns).each do |column|
|
14
|
+
(0...rows).each do |row|
|
15
|
+
image_pixel = @image.pixel_color(column, row)
|
16
|
+
next if image_pixel.transparent?
|
17
|
+
|
18
|
+
color = Color.new(*image_pixel.to_rgb)
|
19
|
+
new_color = new_colors[color]
|
20
|
+
|
21
|
+
next unless new_color
|
22
|
+
|
23
|
+
new_image.pixel_color(column, row, new_color.to_s)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
new_image
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Gauguin
|
2
|
+
class NoiseReducer
|
3
|
+
def call(colors_clusters)
|
4
|
+
pivots = colors_clusters.keys.sort_by! { |key, group| key.percentage }.reverse
|
5
|
+
|
6
|
+
percentage_sum = 0
|
7
|
+
index = 0
|
8
|
+
pivots.each do |color|
|
9
|
+
percentage_sum += color.percentage
|
10
|
+
break if percentage_sum > Gauguin.configuration.min_percentage_sum
|
11
|
+
index += 1
|
12
|
+
end
|
13
|
+
|
14
|
+
reduced_clusters(colors_clusters, pivots, index)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def reduced_clusters(colors_clusters, pivots, cut_off_index)
|
20
|
+
reduced_pivots = pivots[0..cut_off_index]
|
21
|
+
colors_clusters.select do |c|
|
22
|
+
!c.transparent? && reduced_pivots.include?(c)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Gauguin
|
2
|
+
class Painting
|
3
|
+
def initialize(path, image_repository = nil, colors_retriever = nil,
|
4
|
+
colors_limiter = nil, noise_reducer = nil,
|
5
|
+
colors_clusterer = nil, image_recolorer = nil)
|
6
|
+
@image_repository = image_repository || Gauguin::ImageRepository.new
|
7
|
+
@image = @image_repository.get(path)
|
8
|
+
@colors_retriever = colors_retriever || Gauguin::ColorsRetriever.new(@image)
|
9
|
+
@colors_limiter = colors_limiter || Gauguin::ColorsLimiter.new
|
10
|
+
@noise_reducer = noise_reducer || Gauguin::NoiseReducer.new
|
11
|
+
@colors_clusterer = colors_clusterer || Gauguin::ColorsClusterer.new
|
12
|
+
@image_recolorer = image_recolorer || Gauguin::ImageRecolorer.new(@image)
|
13
|
+
end
|
14
|
+
|
15
|
+
def palette
|
16
|
+
colors = @colors_retriever.colors
|
17
|
+
colors = @colors_limiter.call(colors)
|
18
|
+
colors_clusters = @colors_clusterer.clusters(colors)
|
19
|
+
@noise_reducer.call(colors_clusters)
|
20
|
+
end
|
21
|
+
|
22
|
+
def recolor(palette, path)
|
23
|
+
new_colors = @colors_clusterer.reversed_clusters(palette)
|
24
|
+
recolored_image = @image_recolorer.recolor(new_colors)
|
25
|
+
recolored_image.write(path)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Gauguin
|
4
|
+
class PaletteSerializer
|
5
|
+
def self.load(value)
|
6
|
+
return unless value
|
7
|
+
|
8
|
+
value = YAML.load(value)
|
9
|
+
value = value.to_a.map do |color_key, group|
|
10
|
+
[Gauguin::Color.from_a(color_key), group]
|
11
|
+
end
|
12
|
+
value = Hash[value]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.dump(value)
|
16
|
+
value = value.to_a.map { |color, group| [color.to_a, group] }
|
17
|
+
value = Hash[value]
|
18
|
+
YAML.dump(value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|