Repository: LunarLogic/gauguin Branch: master Commit: 4067ee267499 Files: 40 Total size: 41.8 KB Directory structure: gitextract_y36ydtsy/ ├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── gauguin.gemspec ├── lib/ │ ├── gauguin/ │ │ ├── color.rb │ │ ├── color_space/ │ │ │ ├── lab_vector.rb │ │ │ ├── rgb_vector.rb │ │ │ └── xyz_vector.rb │ │ ├── color_space.rb │ │ ├── colors_clusterer.rb │ │ ├── colors_limiter.rb │ │ ├── colors_retriever.rb │ │ ├── image.rb │ │ ├── image_recolorer.rb │ │ ├── image_repository.rb │ │ ├── noise_reducer.rb │ │ ├── painting.rb │ │ ├── palette_serializer.rb │ │ └── version.rb │ └── gauguin.rb └── spec/ ├── integration/ │ ├── painting_spec.rb │ └── samples_spec.rb ├── lib/ │ └── gauguin/ │ ├── color_space/ │ │ ├── rgb_vector_spec.rb │ │ └── xyz_vector_spec.rb │ ├── color_spec.rb │ ├── colors_clusterer_spec.rb │ ├── colors_limiter_spec.rb │ ├── colors_retriever_spec.rb │ ├── image_recolorer_spec.rb │ ├── image_repository_spec.rb │ ├── image_spec.rb │ ├── noise_reducer_spec.rb │ ├── painting_spec.rb │ └── palette_serializer_spec.rb └── spec_helper.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a mkmf.log .DS_Store ================================================ FILE: .rspec ================================================ --color --format documentation ================================================ FILE: .travis.yml ================================================ language: ruby script: - CODECLIMATE_REPO_TOKEN=edbf400c9cd2e92ef8eabf2dad1d03b0ed0cb2a83a20f12f70e4f8107c38de51 bundle exec rake rvm: - 2.1 notifications: email: - anna.slimak@lunarlogic.io ================================================ FILE: Gemfile ================================================ source 'https://rubygems.org' # Specify your gem's dependencies in gauguin.gemspec gemspec ================================================ FILE: Guardfile ================================================ guard :rspec, cmd: 'bundle exec rspec' do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { "spec" } end ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2014 Lunar Logic Polska http://lunarlogicpolska.com MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Build Status](https://travis-ci.org/LunarLogic/gauguin.svg?branch=master)](https://travis-ci.org/LunarLogic/gauguin) [![Code Climate](https://codeclimate.com/github/LunarLogic/gauguin/badges/gpa.svg)](https://codeclimate.com/github/LunarLogic/gauguin) [![Test Coverage](https://codeclimate.com/github/LunarLogic/gauguin/badges/coverage.svg)](https://codeclimate.com/github/LunarLogic/gauguin) Guard Icon # Gauguin Retrieves palette of main colors, merging similar colors using [Lab color space](http://en.wikipedia.org/wiki/Lab_color_space). ## Why not just use `RMagick`? How many colors do you recognize on the image below? ![Black and white image](http://gauguin.lunarlogic.io/assets/gray_and_black-b6871d86ef45c8740bf898233b0a588c.png) Many people would say `2`, but actually there are `1942`. 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. 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. This gem was created to do this for you. ## Sample app Sample application available here: http://gauguin.lunarlogic.io ## Requirements Gem depends on `RMagick` which requires `ImageMagick` to be installed. ### Ubuntu $ sudo apt-get install imagemagick ### OSX $ brew install imagemagick ## Installation Add this line to your application's Gemfile: ```ruby gem 'gauguin' ``` And then execute: $ bundle Or install it yourself as: $ gem install gauguin ## Usage #### Palette ```ruby palette = Gauguin::Painting.new("path/to/image.png").palette ``` Result for image above would be: ```ruby { rgb(204, 204, 204)[0.5900935269505287] => [ rgb(77, 77, 77)[7.383706620723603e-05], rgb(85, 85, 85)[0.00012306177701206005], # ... rgb(219, 220, 219)[1.2306177701206005e-05], rgb(220, 220, 220)[7.383706620723603e-05] ], rgb(0, 0, 0)[0.40990647304947003] => [ rgb(0, 0, 0)[0.40990647304947003], rgb(1, 1, 1)[0.007912872261875462], # ... rgb(64, 64, 64)[6.153088850603002e-05], rgb(66, 66, 66)[6.153088850603002e-05] ] } ``` Where keys are instances of `Gauguin::Color` class and values are array of instances of `Gauguin::Color` class. #### Recolor 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. ```ruby painting.recolor(palette, 'path/where/recolored/file/will/be/placed') ``` ## Custom configuration There are `4` parameters that you can configure: - `max_colors_count` (default value is `10`) - maximum number of colors that a palette will include - `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 - `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 - `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 To configure any of above options you can use configuration block. For example changing `max_colors_count` would look like this: ```ruby Gauguin.configuration do |config| config.max_colors_count = 7 end ``` ## Contributing 1. Fork it ( https://github.com/LunarLogic/gauguin/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request ================================================ FILE: Rakefile ================================================ require "bundler/gem_tasks" require 'rake' require 'rspec/core/rake_task' desc "Run all examples" RSpec::Core::RakeTask.new(:spec) do |t| t.rspec_opts = %w[--color] end task :default => [:spec] ================================================ FILE: gauguin.gemspec ================================================ # coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'gauguin/version' Gem::Specification.new do |spec| spec.name = "gauguin" spec.version = Gauguin::VERSION spec.authors = ["Ania Slimak"] spec.email = ["anna.slimak@lunarlogic.io"] spec.summary = %q{Tool for retrieving main colors from the image.} spec.description = %q{Retrieves palette of main colors, merging similar colors using Lab color space.} spec.homepage = "https://github.com/LunarLogic/gauguin" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] spec.add_dependency "rmagick" spec.add_development_dependency "bundler", "~> 1.7" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rspec", "~> 3.2" spec.add_development_dependency "simplecov" spec.add_development_dependency "codeclimate-test-reporter" spec.add_development_dependency "guard-rspec" spec.add_development_dependency "pry" end ================================================ FILE: lib/gauguin/color.rb ================================================ module Gauguin class Color attr_accessor :red, :green, :blue, :percentage, :transparent def initialize(red, green, blue, percentage = 1, transparent = false) self.red = red self.green = green self.blue = blue self.percentage = percentage self.transparent = transparent end def ==(other) self.class == other.class && self.to_key == other.to_key end alias eql? == def hash self.to_key.hash end def similar?(other_color) self.transparent == other_color.transparent && self.distance(other_color) < Gauguin.configuration.color_similarity_threshold end def distance(other_color) (self.to_lab - other_color.to_lab).r end def to_lab rgb_vector = self.to_vector xyz_vector = rgb_vector.to_xyz xyz_vector.to_lab end def to_vector ColorSpace::RgbVector[*to_rgb] end def to_rgb [self.red, self.green, self.blue] end def to_a to_rgb + [self.percentage, self.transparent] end def self.from_a(array) red, green, blue, percentage, transparent = array Color.new(red, green, blue, percentage, transparent) end def to_key to_rgb + [self.transparent] end def to_s "rgb(#{self.red}, #{self.green}, #{self.blue})" end def inspect msg = "#{to_s}[#{percentage}]" if transparent? msg += "[transparent]" end msg end def transparent? self.transparent end end end ================================================ FILE: lib/gauguin/color_space/lab_vector.rb ================================================ module Gauguin module ColorSpace class LabVector < Vector end end end ================================================ FILE: lib/gauguin/color_space/rgb_vector.rb ================================================ module Gauguin module ColorSpace class RgbVector < Vector MAX_VAUE = 255.0 # Observer. = 2°, Illuminant = D65 RGB_TO_XYZ = Matrix[[0.4124, 0.2126, 0.0193], [0.3576, 0.7152, 0.1192], [0.1805, 0.0722, 0.9505]] def pivot! self.each.with_index do |component, i| self[i] = pivot(component / MAX_VAUE) end self end def to_xyz self.pivot! matrix = Matrix[self] * RGB_TO_XYZ XyzVector[*matrix.row_vectors.first.to_a] end private def pivot(component) component = if component > 0.04045 ((component + 0.055) / 1.055) ** 2.4 else component / 12.92 end component * 100.0 end end end end ================================================ FILE: lib/gauguin/color_space/xyz_vector.rb ================================================ module Gauguin module ColorSpace class XyzVector < Vector WHITE_REFERENCE = self[95.047, 100.000, 108.883] EPSILON = 0.008856 KAPPA = 903.3 def to_lab zipped = self.zip(XyzVector::WHITE_REFERENCE) x, y, z = zipped.map do |component, white_component| component / white_component end l = 116 * f(y) - 16 a = 500 * (f(x) - f(y)) b = 200 * (f(y) - f(z)) LabVector[l, a, b] end private def f(x) if x > EPSILON x ** (1.0/3.0) else (KAPPA * x + 16.0) / 116.0 end end end end end ================================================ FILE: lib/gauguin/color_space.rb ================================================ require 'matrix' require "gauguin/color_space/rgb_vector" require "gauguin/color_space/xyz_vector" require "gauguin/color_space/lab_vector" ================================================ FILE: lib/gauguin/colors_clusterer.rb ================================================ module Gauguin class ColorsClusterer def call(colors) clusters = {} while !colors.empty? pivot = colors.shift group = [pivot] colors, pivot, group = find_all_similar(colors, pivot, group) clusters[pivot] = group end update_pivots_percentages(clusters) clusters end def clusters(colors) clusters = self.call(colors) clusters = clusters.sort_by { |color, _| color.percentage }.reverse Hash[clusters[0...Gauguin.configuration.max_colors_count]] end def reversed_clusters(clusters) reversed_clusters = {} clusters.each do |pivot, group| group.each do |color| reversed_clusters[color] = pivot end end reversed_clusters end private def find_all_similar(colors, pivot, group) loop do similar_colors = colors.select { |c| c.similar?(pivot) } break if similar_colors.empty? group += similar_colors colors -= similar_colors pivot = group.sort_by(&:percentage).last end [colors, pivot, group] end def update_pivots_percentages(clusters) clusters.each do |main_color, group| percentage = group.inject(0) do |sum, color| sum += color.percentage end main_color.percentage = percentage end end end end ================================================ FILE: lib/gauguin/colors_limiter.rb ================================================ module Gauguin class ColorsLimiter def call(colors) colors_limit = Gauguin.configuration.colors_limit if colors.count > colors_limit colors = colors.sort_by { |key, group| key.percentage }. reverse[0..colors_limit - 1] end colors end end end ================================================ FILE: lib/gauguin/colors_retriever.rb ================================================ module Gauguin class ColorsRetriever def initialize(image) @image = image end def colors colors = {} histogram = @image.color_histogram image_size = @image.columns * @image.rows histogram.each do |pixel, count| image_pixel = @image.pixel(pixel) red, green, blue = image_pixel.to_rgb percentage = count.to_f / image_size color = Gauguin::Color.new(red, green, blue, percentage, image_pixel.transparent?) # histogram can contain different magic pixels for # the same colors with different opacity if colors[color] colors[color].percentage += color.percentage else colors[color] = color end end colors.values end end end ================================================ FILE: lib/gauguin/image.rb ================================================ require 'rmagick' require 'forwardable' module Gauguin class Image extend Forwardable attr_accessor :image delegate [:color_histogram, :columns, :rows, :write] => :image def initialize(path = nil) return unless path list = Magick::ImageList.new(path) self.image = list.first end def self.blank(columns, rows) blank_image = Image.new transparent_white = Magick::Pixel.new(255, 255, 255, Pixel::MAX_TRANSPARENCY) blank_image.image = Magick::Image.new(columns, rows) do self.background_color = transparent_white end blank_image end def pixel(magic_pixel) Pixel.new(magic_pixel) end def pixel_color(row, column, *args) magic_pixel = self.image.pixel_color(row, column, *args) pixel(magic_pixel) end class Pixel MAX_CHANNEL_VALUE = 257 MAX_TRANSPARENCY = 65535 def initialize(magic_pixel) @magic_pixel = magic_pixel end def transparent? @magic_pixel.opacity >= MAX_TRANSPARENCY end def to_rgb [:red, :green, :blue].map do |color| @magic_pixel.send(color) / MAX_CHANNEL_VALUE end end end end end ================================================ FILE: lib/gauguin/image_recolorer.rb ================================================ module Gauguin class ImageRecolorer def initialize(image) @image = image.dup end def recolor(new_colors) columns = @image.columns rows = @image.rows new_image = Image.blank(columns, rows) (0...columns).each do |column| (0...rows).each do |row| image_pixel = @image.pixel_color(column, row) next if image_pixel.transparent? color = Color.new(*image_pixel.to_rgb) new_color = new_colors[color] next unless new_color new_image.pixel_color(column, row, new_color.to_s) end end new_image end end end ================================================ FILE: lib/gauguin/image_repository.rb ================================================ module Gauguin class ImageRepository def get(path) Gauguin::Image.new(path) end end end ================================================ FILE: lib/gauguin/noise_reducer.rb ================================================ module Gauguin class NoiseReducer def call(colors_clusters) pivots = colors_clusters.keys.sort_by! { |key, group| key.percentage }.reverse percentage_sum = 0 index = 0 pivots.each do |color| percentage_sum += color.percentage break if percentage_sum > Gauguin.configuration.min_percentage_sum index += 1 end reduced_clusters(colors_clusters, pivots, index) end private def reduced_clusters(colors_clusters, pivots, cut_off_index) reduced_pivots = pivots[0..cut_off_index] colors_clusters.select do |c| !c.transparent? && reduced_pivots.include?(c) end end end end ================================================ FILE: lib/gauguin/painting.rb ================================================ module Gauguin class Painting def initialize(path, image_repository = nil, colors_retriever = nil, colors_limiter = nil, noise_reducer = nil, colors_clusterer = nil, image_recolorer = nil) @image_repository = image_repository || Gauguin::ImageRepository.new @image = @image_repository.get(path) @colors_retriever = colors_retriever || Gauguin::ColorsRetriever.new(@image) @colors_limiter = colors_limiter || Gauguin::ColorsLimiter.new @noise_reducer = noise_reducer || Gauguin::NoiseReducer.new @colors_clusterer = colors_clusterer || Gauguin::ColorsClusterer.new @image_recolorer = image_recolorer || Gauguin::ImageRecolorer.new(@image) end def palette colors = @colors_retriever.colors colors = @colors_limiter.call(colors) colors_clusters = @colors_clusterer.clusters(colors) @noise_reducer.call(colors_clusters) end def recolor(palette, path) new_colors = @colors_clusterer.reversed_clusters(palette) recolored_image = @image_recolorer.recolor(new_colors) recolored_image.write(path) end end end ================================================ FILE: lib/gauguin/palette_serializer.rb ================================================ require 'yaml' module Gauguin class PaletteSerializer def self.load(value) return unless value value = YAML.load(value) value = value.to_a.map do |color_key, group| [Gauguin::Color.from_a(color_key), group] end Hash[value] end def self.dump(value) value = value.to_a.map { |color, group| [color.to_a, group] } value = Hash[value] YAML.dump(value) end end end ================================================ FILE: lib/gauguin/version.rb ================================================ module Gauguin VERSION = "0.0.3" end ================================================ FILE: lib/gauguin.rb ================================================ require "gauguin/version" require "gauguin/color" require "gauguin/color_space" require "gauguin/colors_retriever" require "gauguin/colors_limiter" require "gauguin/colors_clusterer" require "gauguin/noise_reducer" require "gauguin/image_recolorer" require "gauguin/painting" require "gauguin/image" require "gauguin/image_repository" require "gauguin/palette_serializer" module Gauguin class << self attr_accessor :configuration end def self.configure self.configuration ||= Configuration.new yield(configuration) if block_given? end class Configuration DEFAULT_MAX_COLORS_COUNT = 10 DEFAULT_COLORS_LIMIT = 10000 DEFAULT_MIN_PERCENTAGE_SUM = 0.981 DEFAULT_COLOR_SIMILARITY_THRESHOLD = 25 attr_accessor :max_colors_count, :colors_limit, :min_percentage_sum, :color_similarity_threshold def initialize @max_colors_count = DEFAULT_MAX_COLORS_COUNT @colors_limit = DEFAULT_COLORS_LIMIT @min_percentage_sum = DEFAULT_MIN_PERCENTAGE_SUM @color_similarity_threshold = DEFAULT_COLOR_SIMILARITY_THRESHOLD end end end Gauguin.configure ================================================ FILE: spec/integration/painting_spec.rb ================================================ require 'spec_helper' module Gauguin describe Painting do let(:path) do File.join("spec", "support", "pictures", file_name) end let(:gray) { Color.new(204, 204, 204) } let(:black) { Color.new(0, 0, 0) } let(:white) { Color.new(255, 255, 255) } let(:painting) { Painting.new(path) } describe "#palette" do shared_examples_for "retrieves unique colors" do it { expect(subject.count).to eq 5 } it do expect(subject.keys).to include(white) end end subject { painting.palette } context "unique colors in the picture" do let(:file_name) { "unique_colors.png" } it_behaves_like "retrieves unique colors" end context "not unique colors in the picture" do let(:file_name) { "not_unique_colors.png" } it_behaves_like "retrieves unique colors" end context "image has two colors but with different gradients so actually 1942 unique colors" do let(:file_name) { "gray_and_black.png" } let(:values) { subject.values.flatten } it { expect(subject.count).to eq 2 } it { expect(values.include?(black)).to be true } it { expect(values.include?(gray)).to be true } end context "transparent background" do let(:file_name) { "transparent_background.png" } it { expect(subject.count).to eq 1 } it do expect(subject.keys).to eq [Color.new(2, 0, 0)] end end context "image with 10 colors" do let(:file_name) { "10_colors.png" } it { expect(subject.count).to eq 10 } end context "image with over than max_colors_count colors" do let(:file_name) { "12_colors.png" } it { expect(subject.count).to eq 10 } context "image with over than colors_limit colors" do configure(:colors_limit, 9) configure(:max_colors_count, 12) it "returns colors_limit colors" do expect(subject.count).to eq 9 end end end end end end ================================================ FILE: spec/integration/samples_spec.rb ================================================ require 'spec_helper' module Gauguin describe "samples" do def self.picture_path(file_name) File.join("spec", "support", "pictures", file_name) end let(:painting) { Painting.new(picture_path(file_name)) } def self.paths (1..11).map { |i| picture_path(File.join("samples", "sample#{i}.png")) } end def self.expected_results [ ["rgb(219, 12, 38)", "rgb(255, 255, 255)"], ["rgb(168, 36, 40)", "rgb(255, 255, 255)"], ["rgb(0, 0, 0)", "rgb(204, 204, 204)"], ["rgb(154, 79, 54)", "rgb(187, 196, 201)", "rgb(236, 112, 48)", "rgb(28, 28, 64)", "rgb(92, 54, 59)"], ["rgb(254, 254, 254)", "rgb(255, 195, 13)", "rgb(60, 4, 67)"], ["rgb(2, 0, 0)"], ["rgb(148, 158, 149)", "rgb(198, 64, 63)"], ["rgb(109, 207, 246)", "rgb(237, 28, 36)", "rgb(255, 255, 255)"], ["rgb(255, 255, 255)", "rgb(87, 196, 15)"], ["rgb(240, 110, 170)", "rgb(255, 255, 255)"], ["rgb(0, 165, 19)", "rgb(0, 71, 241)", "rgb(230, 27, 49)", "rgb(249, 166, 0)", "rgb(255, 255, 255)"] ] end def self.samples Hash[paths.zip(expected_results)] end samples.each do |sample_path, expected_result| it "returns expected result for #{sample_path}" do painting = Painting.new(sample_path) expect(painting.palette.keys.map(&:to_s).sort).to eq(expected_result.sort) end end end end ================================================ FILE: spec/lib/gauguin/color_space/rgb_vector_spec.rb ================================================ require 'spec_helper' module Gauguin::ColorSpace describe RgbVector do describe "#to_xyz" do let(:red) { RgbVector[255, 0, 0] } it "converts to lab space" do expect(red.to_xyz).to eq( XyzVector[41.24, 21.26, 1.9300000000000002]) end end end end ================================================ FILE: spec/lib/gauguin/color_space/xyz_vector_spec.rb ================================================ require 'spec_helper' module Gauguin::ColorSpace describe XyzVector do describe "#to_lab" do let(:red) { XyzVector[41.24, 21.26, 1.9300000000000002] } it "converts to lab space" do expect(red.to_lab).to eq( LabVector[53.23288178584245, 80.10930952982204, 67.22006831026425]) end end end end ================================================ FILE: spec/lib/gauguin/color_spec.rb ================================================ require 'spec_helper' module Gauguin describe Color do let(:black) { Color.new(0, 0, 0) } let(:red) { Color.new(255, 0, 0) } describe "initialize" do let(:red) { 1 } let(:green) { 2 } let(:blue) { 3 } let(:percentage) { 0.5 } subject { Color.new(red, green, blue, percentage) } it { expect(subject.red).to eq red } it { expect(subject.green).to eq green } it { expect(subject.blue).to eq blue } it { expect(subject.percentage).to eq percentage } end describe "#==" do it "returns true for colors with the same key values" do expect(black == Color.new(0, 0, 0)).to be true end it "returns false if any key value is different" do expect(black == Color.new(0, 0, 1)).to be false end it "returns false for objects with different classes" do expect(black == "black").to be false end end describe "#similar?" do context "similar colors" do it { expect(black.similar?(Color.new(0, 0, 1))).to be true } end context "different colors" do it { expect(black.similar?(red)).to be false } end end describe '#distance' do it 'returns circa 178.36 between black & red' do expect(black.distance(red)).to be_within(0.01).of(117.34) end end describe "#to_lab" do let(:red) { 1 } let(:green) { 2 } let(:blue) { 3 } subject { Color.new(red, green, blue).to_lab } it "returns lab vector" do rgb_vector = double xyz_vector = double expect(ColorSpace::RgbVector).to receive(:[]).with(red, green, blue).and_return(rgb_vector) expect(rgb_vector).to receive(:to_xyz).and_return(xyz_vector) expect(xyz_vector).to receive(:to_lab) subject end end describe "#to_s" do subject { black.to_s } it { expect(subject).to eq("rgb(0, 0, 0)") } end let(:color) { Color.new(1, 2, 3, 0.4, true) } describe "#to_rgb" do subject { color.to_rgb } it { expect(subject).to eq([1, 2, 3]) } end describe "#to_key" do subject { color.to_key } it { expect(subject).to eq([1, 2, 3, true]) } end describe "#to_a" do subject { color.to_a } it { expect(subject).to eq([1, 2, 3, 0.4, true]) } end describe ".from_a" do subject { Color.from_a([1, 2, 3, 0.4, true]) } it { expect(subject.red).to eq(1) } it { expect(subject.green).to eq(2) } it { expect(subject.blue).to eq(3) } it { expect(subject.percentage).to eq(0.4) } it { expect(subject.transparent).to be true } end describe "#transparent?" do subject { color.transparent? } it { expect(subject).to be true } end describe "#hash" do it "can be used as keys in the hash" do hash = { Color.new(255, 255, 255) => 777 } expect(hash[Color.new(255, 255, 255)]).to eq(777) end end describe "#inspect" do subject { color.inspect } it { expect(subject).to eq("rgb(1, 2, 3)[0.4][transparent]")} end end end ================================================ FILE: spec/lib/gauguin/colors_clusterer_spec.rb ================================================ require 'spec_helper' module Gauguin describe ColorsClusterer do let(:black) { Color.new(0, 0, 0, 0.597) } let(:white) { Color.new(255, 255, 255, 0.4) } let(:clusterer) { ColorsClusterer.new } describe "call" do subject { clusterer.call(colors) } context "colors is empty" do let(:colors) { [] } it { expect(subject).to eq({}) } end context "colors includes similar colors" do let(:pseudo_black) { Color.new(4, 0, 0, 0.001) } let(:other_pseudo_black) { Color.new(5, 0, 0, 0.001) } let(:another_pseudo_black) { Color.new(6, 0, 0, 0.001) } let(:colors) do [black, white, pseudo_black, other_pseudo_black, another_pseudo_black] end it "make separate groups for them" do expect(subject).to eq({ white => [white], black => [black, pseudo_black, other_pseudo_black, another_pseudo_black] }) end context do let(:white) { Color.new(255, 255, 255, 0.3) } let(:transparent_white) do Color.new(255, 255, 255, 0.1, Image::Pixel::MAX_TRANSPARENCY) end it "make separate groups for fully transparent colors" do colors << transparent_white expect(subject).to eq({ white => [white], transparent_white => [transparent_white], black => [black, pseudo_black, other_pseudo_black, another_pseudo_black] }) end end it "updates percentage of leader of each group" do subject expect(white.percentage).to eq(0.4) expect(black.percentage).to eq(0.6) end context "there is color with bigger percentage than pivot in the group" do before do black.percentage = 0.001 other_pseudo_black.percentage = 0.597 end it "chooses it as pivot" do expect(subject).to eq({ white => [white], other_pseudo_black => [black, pseudo_black, other_pseudo_black, another_pseudo_black] }) end context "pivots are similar" do before do other_pseudo_black.red = 30 another_pseudo_black.red = 60 end it "merge their groups" do expect(subject).to eq({ white => [white], other_pseudo_black => [black, pseudo_black, other_pseudo_black, another_pseudo_black] }) end end end end context "colors includes different colors" do let(:colors) do [black, white] end before do expect(white).to receive(:similar?). with(black).and_return(false) end it "make separate groups for them" do expect(subject).to eq({ black => [black], white => [white] }) end end end describe "#clusters" do let(:red) { Color.new(255, 0, 0, 0.1) } let(:colors) { [black, red, white] } subject { clusterer.clusters(colors) } configure(:max_colors_count, 2) before do expect(clusterer).to receive(:call).and_return({ black => [black], red => [red], white => [white] }) end it "returns max_colors_count most common colors" do expect(subject).to eq({ white => [white], black => [black] }) end end describe "#reversed_clusters" do let(:gray) { Color.new(0, 0, 10, 0.4) } let(:clusters) do { white => [white], black => [black, gray] } end subject { clusterer.reversed_clusters(clusters) } it "returns reversed clusters" do expect(subject).to eq({ white => white, black => black, gray => black }) end end end end ================================================ FILE: spec/lib/gauguin/colors_limiter_spec.rb ================================================ require 'spec_helper' module Gauguin describe ColorsLimiter do describe "#limit" do let(:limiter) { ColorsLimiter.new } let(:colors) { [black, red, white] } let(:white) { Color.new(255, 255, 255, 0.01) } let(:red) { Color.new(255, 0, 0, 0.02) } let(:black) { Color.new(0, 0, 0, 0.97) } subject { limiter.call(colors) } it "returns all colors" do expect(subject).to eq([black, red, white]) end context "colors count is greater than colors_limit" do configure(:colors_limit, 2) it "reduces colors to colors_limit" do expect(subject).to eq([black, red]) end end end end end ================================================ FILE: spec/lib/gauguin/colors_retriever_spec.rb ================================================ require 'spec_helper' module Gauguin describe ColorsRetriever do let(:retriever) { ColorsRetriever.new(image) } let(:image) do fake = FakeImage.new fake.magic_black_pixel = magic_black_pixel fake.magic_white_pixel = magic_white_pixel fake.magic_red_pixel = magic_red_pixel fake.magic_red_little_transparent_pixel = magic_red_little_transparent_pixel fake.pixels_repository = { magic_white_pixel => FakeImage::Pixel.new(magic_white_pixel), magic_red_pixel => FakeImage::Pixel.new(magic_red_pixel), magic_black_pixel => FakeImage::Pixel.new(magic_black_pixel), magic_red_little_transparent_pixel => FakeImage::Pixel.new( magic_red_little_transparent_pixel) } fake.color_histogram = { magic_white_pixel => 20, magic_black_pixel => 30, magic_red_pixel => 10 } fake.rows = 10 fake.columns = 10 fake end def magic_pixel(rgb, opacity) double(rgb: rgb, opacity: opacity) end let(:magic_black_pixel) { magic_pixel([0, 0, 0], 0) } let(:magic_white_pixel) { magic_pixel([255, 255, 255], 0) } let(:magic_red_pixel) { magic_pixel([255, 0, 0], 0) } let(:magic_red_little_transparent_pixel) { magic_pixel([255, 0, 0], 50) } describe "#colors" do subject { retriever.colors.sort_by(&:percentage) } it "returns array with colors with percentages" do expect(subject).to eq([ Color.new(255, 0, 0, 0.1), Color.new(255, 255, 255, 0.2), Color.new(0, 0, 0, 0.3) ]) end context "histogram contains different magic pixels for the same color with different opacity" do before do image.color_histogram[magic_red_little_transparent_pixel] = 40 end it "sums percentage" do expect(subject).to eq([ Color.new(255, 255, 255, 0.2), Color.new(0, 0, 0, 0.3), Color.new(255, 0, 0, 0.5) ]) end context "fully transparent colors" do let(:magic_red_little_transparent_pixel) do magic_pixel([255, 0, 0], Image::Pixel::MAX_TRANSPARENCY) end it "should be treated separately" do expect(subject).to eq([ Color.new(255, 0, 0, 0.1), Color.new(255, 255, 255, 0.2), Color.new(0, 0, 0, 0.3), Color.new(255, 0, 0, 0.4, true) ]) end end end end end end ================================================ FILE: spec/lib/gauguin/image_recolorer_spec.rb ================================================ require 'spec_helper' module Gauguin describe ImageRecolorer do describe "#recolor" do let(:image) do fake = FakeImage.new fake.pixels = pixels fake.rows = pixels.count fake.columns = pixels.first.count fake.colors_to_pixels = { white.to_s => white_pixel, red.to_s => red_pixel, black.to_s => black_pixel } fake end let(:pixels) do [ [black_pixel, white_pixel, white_pixel], [black_pixel, black_pixel, white_pixel], [black_pixel, black_pixel, black_pixel] ] end let(:image_recolorer) { ImageRecolorer.new(image) } let(:white_pixel) do double('white', to_rgb: [255, 255, 255], transparent?: false) end let(:black_pixel) do double('black', to_rgb: [0, 0, 0], transparent?: false) end let(:red_pixel) do double('red', to_rgb: [255, 0, 0], transparent?: false) end let(:white) { Color.new(255, 255, 255) } let(:black) { Color.new(0, 0, 0) } let(:red) { Color.new(255, 0, 0) } let(:new_colors) do { black => white, white => black } end subject { image_recolorer.recolor(new_colors) } before do allow(Image).to receive(:blank).and_return(image) end it "recolors image based on new_colors" do expect(subject.pixels).to eq([ [white_pixel, black_pixel, black_pixel], [white_pixel, white_pixel, black_pixel], [white_pixel, white_pixel, white_pixel] ]) end context "transparent pixel" do let(:black_pixel) do double('black', to_rgb: [0, 0, 0], transparent?: true) end it "stays the same" do expect(subject.pixels).to eq([ [black_pixel, black_pixel, black_pixel], [black_pixel, black_pixel, black_pixel], [black_pixel, black_pixel, black_pixel] ]) end end context "color not present in new_colors" do let(:pixels) do [ [red_pixel, white_pixel, white_pixel], [red_pixel, red_pixel, white_pixel], [red_pixel, red_pixel, red_pixel] ] end it "stays the same" do expect(subject.pixels).to eq([ [red_pixel, black_pixel, black_pixel], [red_pixel, red_pixel, black_pixel], [red_pixel, red_pixel, red_pixel] ]) end end end end end ================================================ FILE: spec/lib/gauguin/image_repository_spec.rb ================================================ require 'spec_helper' module Gauguin describe ImageRepository do let(:repository) { ImageRepository.new } let(:path) { "path" } describe "#get" do it "returns image" do expect(Gauguin::Image).to receive(:new).with(path) repository.get(path) end end end end ================================================ FILE: spec/lib/gauguin/image_spec.rb ================================================ require 'spec_helper' module Gauguin describe Image do let(:path) do File.join("spec", "support", "pictures", "gray_and_black.png") end let(:image) { Image.new(path) } let(:magic_pixel) { double } describe "initialize" do context "path given" do subject { Image.new(path) } it "returns Image with magick image present" do expect(subject.image).not_to be nil end end context "empty constructor" do subject { Image.new } it "returns Image without magick image" do expect(subject.image).to be nil end end end describe ".blank" do let(:rows) { 10 } let(:columns) { 20 } it "returns blank image with transparent background" do blank_image = Image.blank(rows, columns) pixels = columns.times.map do |column| rows.times.map do |row| blank_image.pixel_color(column, row) end end.flatten expect(pixels.all? { |p| p.transparent? }).to be true end end describe "#pixel" do it "returns new Image::Pixel" do expect(Image::Pixel).to receive(:new) image.pixel(magic_pixel) end end describe "#pixel_color" do subject { image.pixel_color(0, 0) } it "returns Image::Pixel for given row and column" do expect(subject.to_rgb).to eq([204, 204, 204]) end end describe Image::Pixel do let(:pixel) { Image::Pixel.new(magic_pixel) } describe "#transparent?" do let(:magic_pixel) { double(opacity: opacity) } let(:opacity) { 0 } subject { pixel.transparent? } it { expect(subject).to be false } context "opacity equals MAX_TRANSPARENCY" do let(:opacity) { Image::Pixel::MAX_TRANSPARENCY } it { expect(subject).to be true } end end describe "#to_rgb" do let(:magic_pixel) { double(red: 65535, green: 0, blue: 0) } subject { pixel.to_rgb } it { expect(subject).to eq([255, 0, 0]) } end end end end ================================================ FILE: spec/lib/gauguin/noise_reducer_spec.rb ================================================ require 'spec_helper' module Gauguin describe NoiseReducer do let(:reducer) { NoiseReducer.new } describe "#reduce" do subject { reducer.call(colors).keys } let(:white) { Color.new(255, 255, 255, 0.01) } let(:red) { Color.new(255, 0, 0, 0.02) } let(:black) { Color.new(0, 0, 0, 0.97) } let(:colors) do { black => [black], red => [red], white => [white] } end configure(:min_percentage_sum, 0.96) it "returns only relevant colors" do expect(subject).to eq([black]) end context "no sum greater than min_percentage_sum" do let(:white) { Color.new(255, 255, 255, 0.02) } let(:red) { Color.new(255, 0, 0, 0.01) } let(:black) { Color.new(0, 0, 0, 0.90) } it "returns all colors" do expect(subject).to eq([black, red, white]) end end context "transparent color" do configure(:min_percentage_sum, 0.98) before do white.transparent = true end it "returns all colors except white" do expect(subject).to eq([black, red]) end end end end end ================================================ FILE: spec/lib/gauguin/painting_spec.rb ================================================ require 'spec_helper' module Gauguin describe Painting do let(:colors) { [] } let(:image_repository) { double(get: double('image')) } let(:colors_retriever) { double } let(:colors_limiter) { double } let(:noise_reducer) { double } let(:colors_clusterer) { double } let(:image_recolorer) { double } let(:clusters) { {} } let(:painting) do Painting.new("path", image_repository, colors_retriever, colors_limiter, noise_reducer, colors_clusterer, image_recolorer) end describe "#palette" do it "returns hash with main colors of the image" do expect(colors_retriever).to receive(:colors). and_return(colors) expect(colors_limiter).to receive(:call).with(colors). and_return(colors) expect(colors_clusterer).to receive(:clusters).with(colors). and_return(clusters) expect(noise_reducer).to receive(:call).with(clusters). and_return(colors) painting.palette end end describe "#recolor" do let(:palette) { {} } let(:path) { 'path' } let(:image) { double(write: nil) } let(:new_colors) { {} } it "recolors and writes the image to given path" do expect(colors_clusterer).to receive(:reversed_clusters). with(palette).and_return(new_colors) expect(image_recolorer).to receive(:recolor). with(new_colors).and_return(image) expect(image).to receive(:write).with(path) painting.recolor(palette, path) end end end end ================================================ FILE: spec/lib/gauguin/palette_serializer_spec.rb ================================================ require 'spec_helper' module Gauguin describe PaletteSerializer do let(:palette) do { Color.new(255, 255, 255, 0.7, true) => [Color.new(255, 0, 0, 0.7, true)] } end it "serializes palette" do dumped = PaletteSerializer.dump(palette) loaded = PaletteSerializer.load(dumped) key = loaded.keys.first value = loaded.values.first.first expect(key.class).to eq(Color) expect(value.class).to eq(Color) expect(key.to_a).to eq([255, 255, 255, 0.7, true]) expect(value.to_a).to eq([255, 0, 0, 0.7, true]) end end end ================================================ FILE: spec/spec_helper.rb ================================================ if ENV['CODECLIMATE_REPO_TOKEN'] require "codeclimate-test-reporter" CodeClimate::TestReporter.start else require 'simplecov' SimpleCov.start end require 'bundler/setup' require './lib/gauguin' require 'pry' Bundler.setup RSpec.configure do |config| end def configure(config_option, value) old_value = Gauguin.configuration.send(config_option) before do Gauguin.configuration.send("#{config_option}=", value) end after do Gauguin.configuration.send("#{config_option}=", old_value) end end class FakeImage attr_accessor :magic_black_pixel, :magic_red_pixel, :magic_white_pixel, :magic_red_little_transparent_pixel, :pixels_repository, :color_histogram, :rows, :columns, :pixels, :colors_to_pixels def pixel(magic_pixel) pixels_repository[magic_pixel] end def pixel_color(row, column, new_color = nil) if new_color new_pixel = self.colors_to_pixels[new_color] pixels[row][column] = new_pixel end pixels[row][column] end class Pixel < Gauguin::Image::Pixel attr_accessor :magic_pixel def initialize(magic_pixel) self.magic_pixel = magic_pixel end def to_rgb magic_pixel.rgb end end end