gauguin 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+