gauguin 0.0.2

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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +9 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +6 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +119 -0
  9. data/Rakefile +13 -0
  10. data/gauguin.gemspec +30 -0
  11. data/lib/gauguin/color.rb +76 -0
  12. data/lib/gauguin/color_space/lab_vector.rb +6 -0
  13. data/lib/gauguin/color_space/rgb_vector.rb +36 -0
  14. data/lib/gauguin/color_space/xyz_vector.rb +33 -0
  15. data/lib/gauguin/color_space.rb +4 -0
  16. data/lib/gauguin/colors_clusterer.rb +63 -0
  17. data/lib/gauguin/colors_limiter.rb +14 -0
  18. data/lib/gauguin/colors_retriever.rb +33 -0
  19. data/lib/gauguin/image.rb +55 -0
  20. data/lib/gauguin/image_recolorer.rb +29 -0
  21. data/lib/gauguin/image_repository.rb +7 -0
  22. data/lib/gauguin/noise_reducer.rb +26 -0
  23. data/lib/gauguin/painting.rb +29 -0
  24. data/lib/gauguin/palette_serializer.rb +22 -0
  25. data/lib/gauguin/version.rb +3 -0
  26. data/lib/gauguin.rb +43 -0
  27. data/spec/integration/painting_spec.rb +79 -0
  28. data/spec/integration/samples_spec.rb +43 -0
  29. data/spec/lib/gauguin/color_space/rgb_vector_spec.rb +15 -0
  30. data/spec/lib/gauguin/color_space/xyz_vector_spec.rb +15 -0
  31. data/spec/lib/gauguin/color_spec.rb +125 -0
  32. data/spec/lib/gauguin/colors_clusterer_spec.rb +158 -0
  33. data/spec/lib/gauguin/colors_limiter_spec.rb +27 -0
  34. data/spec/lib/gauguin/colors_retriever_spec.rb +85 -0
  35. data/spec/lib/gauguin/image_recolorer_spec.rb +94 -0
  36. data/spec/lib/gauguin/image_repository_spec.rb +15 -0
  37. data/spec/lib/gauguin/image_spec.rb +90 -0
  38. data/spec/lib/gauguin/noise_reducer_spec.rb +51 -0
  39. data/spec/lib/gauguin/painting_spec.rb +55 -0
  40. data/spec/lib/gauguin/palette_serializer_spec.rb +24 -0
  41. data/spec/spec_helper.rb +60 -0
  42. data/spec/support/pictures/10_colors.png +0 -0
  43. data/spec/support/pictures/12_colors.png +0 -0
  44. data/spec/support/pictures/gauguin.png +0 -0
  45. data/spec/support/pictures/gray_and_black.png +0 -0
  46. data/spec/support/pictures/not_unique_colors.png +0 -0
  47. data/spec/support/pictures/samples/sample1.png +0 -0
  48. data/spec/support/pictures/samples/sample10.png +0 -0
  49. data/spec/support/pictures/samples/sample11.png +0 -0
  50. data/spec/support/pictures/samples/sample2.png +0 -0
  51. data/spec/support/pictures/samples/sample3.png +0 -0
  52. data/spec/support/pictures/samples/sample4.png +0 -0
  53. data/spec/support/pictures/samples/sample5.png +0 -0
  54. data/spec/support/pictures/samples/sample6.png +0 -0
  55. data/spec/support/pictures/samples/sample7.png +0 -0
  56. data/spec/support/pictures/samples/sample8.png +0 -0
  57. data/spec/support/pictures/samples/sample9.png +0 -0
  58. data/spec/support/pictures/too_many_colors.png +0 -0
  59. data/spec/support/pictures/transparent_background.png +0 -0
  60. data/spec/support/pictures/unique_colors.png +0 -0
  61. 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
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ script:
3
+ - CODECLIMATE_REPO_TOKEN=edbf400c9cd2e92ef8eabf2dad1d03b0ed0cb2a83a20f12f70e4f8107c38de51 bundle exec rake
4
+ rvm:
5
+ - 2.1
6
+ notifications:
7
+ email:
8
+ - anna.slimak@lunarlogic.io
9
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in gauguin.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard :rspec, cmd: 'bundle exec rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
6
+
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
@@ -0,0 +1,13 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake'
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc "Run all examples"
7
+ RSpec::Core::RakeTask.new(:spec) do |t|
8
+ t.rspec_opts = %w[--color]
9
+ end
10
+
11
+
12
+ task :default => [:spec]
13
+
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,6 @@
1
+ module Gauguin
2
+ module ColorSpace
3
+ class LabVector < Vector
4
+ end
5
+ end
6
+ end
@@ -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,4 @@
1
+ require 'matrix'
2
+ require "gauguin/color_space/rgb_vector"
3
+ require "gauguin/color_space/xyz_vector"
4
+ require "gauguin/color_space/lab_vector"
@@ -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,7 @@
1
+ module Gauguin
2
+ class ImageRepository
3
+ def get(path)
4
+ Gauguin::Image.new(path)
5
+ end
6
+ end
7
+ 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
+