camalian 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d6d9583383ecf60a0cd259bcce471abe319f2b4390b52c53d63a2504f3dd020f
4
+ data.tar.gz: 6e5cb3a2c174539b91bae01fafad0e9ef658756bebc7809837cc0edb019cfacd
5
+ SHA512:
6
+ metadata.gz: c79fb3ea3e499fee16fa36f8e10c640a54a1ddc3d0121a99d5053b7c28a60cfbe903f2600c59ec61997a42942f40b16298714904dfbbdb170dbbfa0adf23540b
7
+ data.tar.gz: 4d7a82621d0429c005f4d3743724bc2de51c7f57110416d50a1397de8d6a473e73ec25416f53533b12c500670709412cee6fd73b529fc68dfc044e5112f54578
@@ -0,0 +1,30 @@
1
+ name: Github Packages Push
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ tags:
7
+ - '*'
8
+ jobs:
9
+ build:
10
+ name: Build + Publish
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - name: Set up Ruby 2.6
16
+ uses: actions/setup-ruby@v1
17
+ with:
18
+ ruby-version: 2.6.x
19
+
20
+ - name: Publish to GPR
21
+ run: |
22
+ mkdir -p $HOME/.gem
23
+ touch $HOME/.gem/credentials
24
+ chmod 0600 $HOME/.gem/credentials
25
+ printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
26
+ gem build *.gemspec
27
+ gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
28
+ env:
29
+ GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}"
30
+ OWNER: ${{ github.repository_owner }}
@@ -0,0 +1,32 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: build
9
+
10
+ on:
11
+ push:
12
+ branches: [ master ]
13
+ pull_request:
14
+ branches: [ master ]
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-16.04
19
+ strategy:
20
+ matrix:
21
+ ruby: [ '2.4', '2.5', '2.6', '2.7' ]
22
+ name: Ruby ${{ matrix.ruby }}
23
+ steps:
24
+ - uses: actions/checkout@v2
25
+ - uses: actions/setup-ruby@v1
26
+ with:
27
+ ruby-version: ${{ matrix.ruby }}
28
+ - name: Install dependencies
29
+ run: bundle install
30
+ - name: Run tests
31
+ run: bundle exec rake
32
+
@@ -0,0 +1,29 @@
1
+ name: Ruby Gems Push
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ tags:
7
+ - '*'
8
+ jobs:
9
+ build:
10
+ name: Build + Publish
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - name: Set up Ruby 2.6
16
+ uses: actions/setup-ruby@v1
17
+ with:
18
+ ruby-version: 2.6.x
19
+
20
+ - name: Publish to RubyGems
21
+ run: |
22
+ mkdir -p $HOME/.gem
23
+ touch $HOME/.gem/credentials
24
+ chmod 0600 $HOME/.gem/credentials
25
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
26
+ gem build *.gemspec
27
+ gem push *.gem
28
+ env:
29
+ GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
data/.gitignore CHANGED
@@ -15,4 +15,5 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
- .idea/
18
+ .idea/
19
+ coverage/
data/.overcommit.yml ADDED
@@ -0,0 +1,33 @@
1
+ # Use this file to configure the Overcommit hooks you wish to use. This will
2
+ # extend the default configuration defined in:
3
+ # https://github.com/sds/overcommit/blob/master/config/default.yml
4
+ #
5
+ # At the topmost level of this YAML file is a key representing type of hook
6
+ # being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
7
+ # customize each hook, such as whether to only run it on certain files (via
8
+ # `include`), whether to only display output if it fails (via `quiet`), etc.
9
+ #
10
+ # For a complete list of hooks, see:
11
+ # https://github.com/sds/overcommit/tree/master/lib/overcommit/hook
12
+ #
13
+ # For a complete list of options that you can use to customize hooks, see:
14
+ # https://github.com/sds/overcommit#configuration
15
+ #
16
+ # Uncomment the following lines to make the configuration take effect.
17
+
18
+ PreCommit:
19
+ RuboCop:
20
+ enabled: true
21
+ on_warn: fail # Treat all warnings as failures
22
+
23
+ # TrailingWhitespace:
24
+ # enabled: true
25
+ # exclude:
26
+ # - '**/db/structure.sql' # Ignore trailing whitespace in generated files
27
+ #
28
+ #PostCheckout:
29
+ # ALL: # Special hook name that customizes all hooks of this type
30
+ # quiet: true # Change all post-checkout hooks to only display output on failure
31
+ #
32
+ # IndexTags:
33
+ # enabled: true # Generate a tags file with `ctags` each time HEAD changes
data/.rubocop.yml ADDED
@@ -0,0 +1,37 @@
1
+ # The behavior of RuboCop can be controlled via the .rubocop.yml
2
+ # configuration file. It makes it possible to enable/disable
3
+ # certain cops (checks) and to alter their behavior if they accept
4
+ # any parameters. The file can be placed either in your home
5
+ # directory or in some project directory.
6
+ #
7
+ # RuboCop will start looking for the configuration file in the directory
8
+ # where the inspected file is and continue its way up to the root directory.
9
+ #
10
+ # See https://docs.rubocop.org/rubocop/configuration
11
+
12
+ AllCops:
13
+ NewCops: enable
14
+
15
+ Naming/MethodParameterName:
16
+ AllowedNames:
17
+ - r
18
+ - g
19
+ - b
20
+
21
+ Metrics/MethodLength:
22
+ Max: 50
23
+
24
+ Metrics/PerceivedComplexity:
25
+ Max: 25
26
+
27
+ Metrics/CyclomaticComplexity:
28
+ Max: 15
29
+
30
+ Metrics/BlockLength:
31
+ Exclude:
32
+ - test/**/*
33
+
34
+ Metrics/AbcSize:
35
+ Max: 30
36
+ IgnoredMethods:
37
+ - build_components
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 2.7.3
data/.travis.yml ADDED
@@ -0,0 +1,17 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.7
4
+ - 2.6
5
+ - 2.5
6
+ - 2.4
7
+
8
+ before_script:
9
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
10
+ - chmod +x ./cc-test-reporter
11
+ - ./cc-test-reporter before-build
12
+
13
+ script:
14
+ - bundle exec rake test
15
+
16
+ after_script:
17
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
data/Gemfile CHANGED
@@ -1,4 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in camalian.gemspec
4
6
  gemspec
7
+
8
+ gem 'overcommit', require: false
9
+ gem 'rubocop', '~> 1.3', require: false
10
+ gem 'rubocop-daemon', require: false
11
+ gem 'simplecov', require: false
data/LICENSE.txt CHANGED
File without changes
data/README.md CHANGED
@@ -1,12 +1,16 @@
1
1
  # Camalian
2
2
 
3
- TODO: Write a gem description
3
+ [![Gem Version](https://badge.fury.io/rb/camalian.svg)](https://badge.fury.io/rb/camalian)
4
+ ![Ruby](https://github.com/nazarhussain/camalian/workflows/build/badge.svg?branch=master)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/5495a2c122469d81b6c5/maintainability)](https://codeclimate.com/github/nazarhussain/camalian/maintainability)
6
+
7
+ Ruby gem to extract color palettes from images and play with their saturation
4
8
 
5
9
  ## Installation
6
10
 
7
11
  Add this line to your application's Gemfile:
8
12
 
9
- gem 'camalian'
13
+ gem 'camalian', '~> 0.2.0'
10
14
 
11
15
  And then execute:
12
16
 
@@ -18,7 +22,46 @@ Or install it yourself as:
18
22
 
19
23
  ## Usage
20
24
 
21
- TODO: Write usage instructions here
25
+ ```ruby
26
+ image = Camalian::load('file_path')
27
+ colors = image.prominent_colors(15)
28
+ colors = colors.sort_similar_colors
29
+ colors.light_colors(0, 40)
30
+ ```
31
+
32
+ You can find a working example with detail explanation and reference code here on [this link](https://basicdrift.com/color-extraction-library-build-color-search-engine-fdf369678d5a). Here we will build a functional color based image search engine in Ruby on Rails.
33
+
34
+ NOTE: Since its a compute intensive operation so for production use its suggested to use under a background job and not within a request/response cycle.
35
+
36
+ ## Quantization Algorithms
37
+
38
+ Currently following algorithms are implemented.
39
+
40
+ ### Histogram
41
+
42
+ Its a most common algorithm for color quantization and used different bucket technique to group the colors together. You can read more about this [technique here](https://en.wikipedia.org/wiki/Color_histogram). It can be accessed by `Camalian::QUANTIZATION_HISTOGRAM` constant. This is used as default method as well.
43
+
44
+ ### K Means
45
+
46
+ This algorithm uses color distancing in RGB space to group the similar colors. You can learn more about this [technique here](https://en.wikipedia.org/wiki/K-means_clustering). It can be accessed by `Camalian::QUANTIZATION_K_MEANS` constant.
47
+
48
+ ### Median Cut
49
+
50
+ This algorithm uses color highest color range to determine the median and split colors to groups. The output consists of average color of such color groups. Since these algorithm don't use actual colors and instead average, so you will may not exact matching pixel in the image. This algorithm is nice to be used with image compression, where similarity and compression is important than having same pixel colors. You can learn more about this [technique here](https://tpgit.github.io/UnOfficialLeptDocs/leptonica/color-quantization.html) . It can be accessed by `Camalian::QUANTIZATION_MEDIAN_CUT` constant.
51
+
52
+
53
+ You can set default quantization method globally as:
54
+
55
+ ```ruby
56
+ Camalian.options[:quantization] = Camalian::QUANTIZATION_K_MEANS
57
+ ```
58
+
59
+ or you can set at the time of extracting colors by.
60
+
61
+ ```ruby
62
+ image = Camalian::load('file_path')
63
+ colors = image.prominent_colors(15, quantization: Camalian::QUANTIZATION_K_MEANS)
64
+ ```
22
65
 
23
66
  ## Contributing
24
67
 
data/Rakefile CHANGED
@@ -1 +1,13 @@
1
- require "bundler/gem_tasks"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ require 'rake/testtask'
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs << 'test'
9
+ t.pattern = 'test/**/*_test.rb'
10
+ end
11
+
12
+ desc 'Run tests'
13
+ task default: :test
data/camalian.gemspec CHANGED
@@ -1,21 +1,28 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'camalian/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "camalian"
8
+ spec.name = 'camalian'
8
9
  spec.version = Camalian::VERSION
9
- spec.authors = ["Nazar Hussain"]
10
- spec.email = ["nazarhussain@gmail.com"]
11
- spec.description = %q{Library used to deal with colors and images}
12
- spec.summary = %q{Library used to deal with colors and images. You can extract colors from images.}
13
- spec.homepage = "http://www.nazarhussain.com"
14
- spec.license = "MIT"
10
+ spec.authors = ['Nazar Hussain']
11
+ spec.email = ['nazarhussain@gmail.com']
12
+ spec.description = 'Library used to deal with colors and images'
13
+ spec.summary = 'Library used to deal with colors and images. You can extract colors from images.'
14
+ spec.homepage = 'https://github.com/nazarhussain/camalian'
15
+ spec.license = 'MIT'
15
16
 
16
- spec.files = `git ls-files`.split($/)
17
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
18
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_dependency 'chunky_png', '~> 1.3', '>= 1.3.14'
23
+
24
+ spec.add_development_dependency 'minitest', '~> 5.14', '>= 5.14.2'
25
+ spec.add_development_dependency 'rake', '~> 13.0', '>= 13.0.1'
20
26
 
27
+ spec.required_ruby_version = '>= 2.4'
21
28
  end
data/lib/camalian.rb CHANGED
@@ -1,5 +1,32 @@
1
- require "camalian/version"
1
+ # frozen_string_literal: true
2
2
 
3
- module Camalian
4
- # Your code goes here...
3
+ require 'chunky_png'
4
+ require 'tempfile'
5
+ require 'open-uri'
6
+
7
+ require 'camalian/version'
8
+ require 'camalian/color'
9
+ require 'camalian/palette'
10
+ require 'camalian/image'
11
+ require 'camalian/quantization/histogram'
12
+ require 'camalian/quantization/k_means'
13
+ require 'camalian/quantization/median_cut'
14
+
15
+ module Camalian # :nodoc:
16
+ QUANTIZATION_HISTOGRAM = Camalian::Quantization::Histogram
17
+ QUANTIZATION_K_MEANS = Camalian::Quantization::KMeans
18
+ QUANTIZATION_MEDIAN_CUT = Camalian::Quantization::MedianCut
19
+
20
+ class << self
21
+ def options
22
+ @options ||= {
23
+ color_count: 8,
24
+ quantization: Camalian::QUANTIZATION_HISTOGRAM
25
+ }
26
+ end
27
+
28
+ def load(image_path)
29
+ Image.new(image_path)
30
+ end
31
+ end
5
32
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camalian
4
+ # Camalian color object
5
+ class Color
6
+ attr_reader :r, :g, :b, :h, :s, :l, :hsv
7
+
8
+ def initialize(r, g, b)
9
+ build_components(r, g, b)
10
+ end
11
+
12
+ def self.from_hex(hex_value)
13
+ color_hash = hex_value[0..6]
14
+ color_hash = color_hash[1..6] if color_hash[0] == '#'
15
+ r = color_hash[0..1].to_i(16)
16
+ g = color_hash[2..3].to_i(16)
17
+ b = color_hash[4..5].to_i(16)
18
+
19
+ Color.new(r, g, b)
20
+ end
21
+
22
+ def to_s
23
+ "red=#{r} green=#{g} blue=#{b} hue=#{h} saturation=#{s} lightness=#{l}"
24
+ end
25
+
26
+ def to_hex
27
+ "##{r.to_s(16).rjust(2, '0')}#{g.to_s(16).rjust(2, '0')}#{b.to_s(16).rjust(2, '0')}"
28
+ end
29
+
30
+ # Used for array uniqueness
31
+ def hash
32
+ "#{r}#{g}#{b}".to_i
33
+ end
34
+
35
+ # Used for object comparison
36
+ def ==(other)
37
+ r == other.r && g == other.g && b == other.b
38
+ end
39
+ alias eql? ==
40
+
41
+ def hue_distance(color)
42
+ [(h - color.h) % 360, (color.h - h) % 360].min
43
+ end
44
+
45
+ def rgb_distance(color)
46
+ Math.sqrt(((r - color.r)**2) + ((g - color.g)**2) + ((b - color.b)**2))
47
+ end
48
+
49
+ private
50
+
51
+ def build_components(r, g, b)
52
+ @r = r
53
+ @g = g
54
+ @b = b
55
+
56
+ ri = @r / 255.0
57
+ gi = @g / 255.0
58
+ bi = @b / 255.0
59
+
60
+ cmax = [ri, gi, bi].max
61
+ cmin = [ri, gi, bi].min
62
+ delta = cmax - cmin
63
+
64
+ @l = (cmax + cmin) / 2.0
65
+
66
+ if delta.zero?
67
+ @h = 0
68
+ elsif cmax == ri
69
+ @h = 60 * (((gi - bi) / delta) % 6)
70
+ elsif cmax == gi
71
+ @h = 60 * (((bi - ri) / delta) + 2)
72
+ elsif cmax == bi
73
+ @h = 60 * (((ri - gi) / delta) + 4)
74
+ end
75
+
76
+ @s = if delta.zero?
77
+ 0
78
+ else
79
+ delta / (1 - (2 * @l - 1).abs)
80
+ end
81
+
82
+ @h = @h.round(2)
83
+ @s = (@s * 100).round(2)
84
+ @l = (@l * 100).round(2)
85
+
86
+ # HSV Calculation
87
+ # Hue calculation
88
+ if delta.zero?
89
+ @hsv = [0]
90
+ elsif cmax == ri
91
+ @hsv = [60 * (((gi - bi) / delta) % 6)]
92
+ elsif cmax == gi
93
+ @hsv = [60 * (((bi - ri) / delta) + 2)]
94
+ elsif cmax == bi
95
+ @hsv = [60 * (((ri - gi) / delta) + 4)]
96
+ end
97
+
98
+ # Saturation calculation
99
+ @hsv << if cmax.zero?
100
+ 0
101
+ else
102
+ delta / cmax
103
+ end
104
+
105
+ # Value calculation
106
+ @hsv << cmax
107
+
108
+ @hsv = [@hsv[0].round(2), (@hsv[1] * 100).round(2), (@hsv[2] * 100).round(2)]
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camalian
4
+ # Load image into Camalian
5
+ class Image
6
+ attr_accessor :src_file_path
7
+
8
+ def initialize(file_path)
9
+ @src_file_path = file_path
10
+ end
11
+
12
+ def prominent_colors(count = Camalian.options[:color_count],
13
+ quantization: Camalian.options[:quantization],
14
+ optimal: true)
15
+ image = ::ChunkyPNG::Image.from_file(@src_file_path)
16
+
17
+ colors = image.pixels.map do |val|
18
+ Color.new(
19
+ ChunkyPNG::Color.r(val),
20
+ ChunkyPNG::Color.g(val),
21
+ ChunkyPNG::Color.b(val)
22
+ )
23
+ end
24
+
25
+ quantize = quantization.new
26
+
27
+ palette = quantize.process(colors, count)
28
+
29
+ retry_count = 1
30
+ while !optimal && palette.size < count
31
+ palette = quantize.process(colors, count + retry_count)
32
+ retry_count += 1
33
+ end
34
+
35
+ palette
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camalian
4
+ # Collection of colors with some useful features
5
+ class Palette < Array
6
+ def self.from_hex(hex_values)
7
+ new(hex_values.map { |v| Color.from_hex(v) })
8
+ end
9
+
10
+ def sort_by_lightness
11
+ Palette.new(sort_by(&:l).reverse)
12
+ end
13
+
14
+ def sort_by_hue
15
+ Palette.new(sort_by(&:h).reverse)
16
+ end
17
+
18
+ def sort_similar_colors
19
+ Palette.new(sort_by(&:hsv))
20
+ end
21
+
22
+ def sort_by_saturation
23
+ Palette.new(sort_by(&:s).reverse)
24
+ end
25
+
26
+ def average_color
27
+ r = map(&:r).inject(&:+)
28
+ g = map(&:g).inject(&:+)
29
+ b = map(&:b).inject(&:+)
30
+ size = self.size
31
+
32
+ Color.new(r / size, g / size, b / size)
33
+ end
34
+
35
+ def light_colors(limit1, limit2)
36
+ min = [limit1, limit2].min
37
+ max = [limit1, limit2].max
38
+ table = dup
39
+ Palette.new(table.delete_if { |color| color.l > max or color.l < min })
40
+ end
41
+
42
+ def to_hex
43
+ map(&:to_hex)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camalian
4
+ module Quantization
5
+ class Histogram # :nodoc:
6
+ def process(colors, count)
7
+ bucket_size = (255.0 / count).ceil
8
+ buckets = {}
9
+
10
+ colors.each do |color|
11
+ key = bucket_key(color, bucket_size)
12
+ buckets[key] ||= Palette.new
13
+ buckets[key].push(color)
14
+ end
15
+
16
+ Palette.new(buckets.map { |_, value| value.average_color }[0...count])
17
+ end
18
+
19
+ private
20
+
21
+ def bucket_key(color, bucket_size)
22
+ "#{color.r / bucket_size}:#{color.g / bucket_size}:#{color.b / bucket_size}"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camalian
4
+ module Quantization
5
+ class KMeans # :nodoc:
6
+ INIT_TRIES = 10
7
+
8
+ def process(colors, count)
9
+ # Its faster to extract unique colors once
10
+ colors = colors.uniq
11
+ means = initial_means(colors, count)
12
+
13
+ done = false
14
+
15
+ until done
16
+ groups = group_by_means(colors, means)
17
+ new_means = groups.map do |group|
18
+ Palette.new(group).average_color
19
+ end
20
+ common = means & new_means
21
+
22
+ done = common.size == means.size
23
+ means = new_means
24
+ end
25
+
26
+ Palette.new(means)
27
+ end
28
+
29
+ private
30
+
31
+ def initial_means(colors, count)
32
+ count = colors.size if count > colors.size
33
+ means = []
34
+ tries = 0
35
+ while means.size != count && tries < INIT_TRIES
36
+ (count - means.size).times { means << colors[rand(colors.size)] }
37
+ means.uniq!
38
+ tries += 1
39
+ end
40
+
41
+ means
42
+ end
43
+
44
+ def group_by_means(pixels, means)
45
+ groups = {}
46
+
47
+ pixels.each do |p|
48
+ distances = means.map { |m| p.rgb_distance(m) }
49
+ min_distance_index = distances.index(distances.min)
50
+
51
+ groups[min_distance_index] ||= []
52
+ groups[min_distance_index] << p
53
+ end
54
+
55
+ groups.values
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Camalian
4
+ module Quantization
5
+ class MedianCut # :nodoc:
6
+ SplitInfo = Struct.new(:range, :group_index, :color, keyword_init: true)
7
+ def process(colors, count)
8
+ # Its faster to extract unique colors once
9
+ colors = colors.uniq
10
+ groups = [colors]
11
+ limit = [count, colors.size].min
12
+
13
+ loop do
14
+ split_info = determine_group_split(groups)
15
+ group1, group2 = split_group(groups[split_info.group_index], split_info)
16
+ groups.reject!.each_with_index { |_g, index| index == split_info.group_index }
17
+ groups << group1 unless group1.empty?
18
+ groups << group2 unless group2.empty?
19
+
20
+ break if groups.size >= limit
21
+ end
22
+
23
+ palletes = groups.map { |g| Palette.new(g.uniq) }
24
+ average_colors = palletes.map(&:average_color)
25
+
26
+ Palette.new(average_colors[0...count])
27
+ end
28
+
29
+ private
30
+
31
+ def determine_group_split(groups)
32
+ stats = []
33
+
34
+ groups.each_with_index do |group, index|
35
+ reds = group.map(&:r)
36
+ greens = group.map(&:g)
37
+ blues = group.map(&:b)
38
+
39
+ ranges = []
40
+ ranges << SplitInfo.new(group_index: index, range: reds.max - reds.min, color: :r) unless reds.empty?
41
+ ranges << SplitInfo.new(group_index: index, range: greens.max - greens.min, color: :g) unless greens.empty?
42
+ ranges << SplitInfo.new(group_index: index, range: blues.max - blues.min, color: :b) unless blues.empty?
43
+
44
+ stats << ranges.max_by(&:range)
45
+ end
46
+
47
+ stats.max_by(&:range)
48
+ end
49
+
50
+ def split_group(group, split_info)
51
+ split_color = split_info.color
52
+ colors = group.sort_by { |pixel| pixel.send split_color }
53
+
54
+ return [[colors[0]], [colors[1]]] if colors.size == 2
55
+
56
+ median_index = colors.size / 2
57
+ median_value = colors[median_index].send split_color
58
+
59
+ group1 = []
60
+ group2 = []
61
+
62
+ colors.each do |color|
63
+ if color.send(split_color) <= median_value
64
+ group1 << color
65
+ else
66
+ group2 << color
67
+ end
68
+ end
69
+
70
+ [group1, group2]
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Camalian
2
- VERSION = "0.0.1"
4
+ VERSION = '0.2.0'
3
5
  end
Binary file
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+ require 'camalian'
7
+
8
+ describe Camalian::Color do
9
+ before do
10
+ @color = Camalian::Color.new(120, 255, 30)
11
+ end
12
+
13
+ describe 'Color initialized with 120, 255, 30 rgb values' do
14
+ it 'hex value must be #78ff1e' do
15
+ _(@color.to_hex).must_equal '#78ff1e'
16
+ end
17
+
18
+ it 'hsl color components must ' do
19
+ _([@color.h.to_i, @color.s.to_i, @color.l.to_i]).must_equal [96, 100, 55]
20
+ end
21
+
22
+ it 'hsv color components must ' do
23
+ _(@color.hsv.map(&:to_i)).must_equal [96, 88, 100]
24
+ end
25
+ end
26
+
27
+ describe 'initialized with 1 integer rgb value' do
28
+ it 'must have leading zero' do
29
+ _(Camalian::Color.new(7, 7, 7).to_hex).must_equal '#070707'
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+ require 'camalian'
7
+
8
+ describe Camalian::Palette do
9
+ before do
10
+ mixed_colors = %w[#2560a2 #4dd915 #1530db #49cc23 #45c031 #3da84d #1d48be #399c5b #359069 #318478 #2d7886 #296c94
11
+ #2154b0 #193ccc #41b43f].freeze
12
+
13
+ @palette = Camalian::Palette.from_hex(mixed_colors)
14
+ end
15
+
16
+ it 'initialize palette with correct size' do
17
+ _(@palette.size).must_equal 15
18
+ end
19
+
20
+ describe '#sort_by_lightness' do
21
+ it 'sort colors by lightness ' do
22
+ _(@palette.sort_by_lightness.to_hex).must_equal ['#41b43f', '#45c031', '#1530db', '#49cc23', '#4dd915',
23
+ '#193ccc', '#3da84d', '#1d48be', '#399c5b', '#2154b0',
24
+ '#2560a2', '#359069', '#296c94', '#318478', '#2d7886']
25
+ end
26
+ end
27
+
28
+ describe '#sort_by_hue' do
29
+ it 'sort colors by hue' do
30
+ _(@palette.sort_by_hue.to_hex).must_equal ['#1530db', '#193ccc', '#1d48be', '#2154b0', '#2560a2', '#296c94',
31
+ '#2d7886', '#318478', '#359069', '#399c5b', '#3da84d', '#41b43f',
32
+ '#45c031', '#49cc23', '#4dd915']
33
+ end
34
+ end
35
+
36
+ describe '#sort_by_saturation' do
37
+ it 'sort colors by saturation' do
38
+ _(@palette.sort_by_saturation.to_hex).must_equal ['#1530db', '#4dd915', '#193ccc', '#1d48be', '#49cc23',
39
+ '#2154b0', '#2560a2', '#45c031', '#296c94', '#2d7886',
40
+ '#41b43f', '#3da84d', '#399c5b', '#359069', '#318478']
41
+ end
42
+ end
43
+
44
+ describe '#sort_similar_colors' do
45
+ it 'sort colors by hsv' do
46
+ _(@palette.sort_similar_colors.to_hex).must_equal ['#4dd915', '#49cc23', '#45c031', '#41b43f', '#3da84d',
47
+ '#399c5b', '#359069', '#318478', '#2d7886', '#296c94',
48
+ '#2560a2', '#2154b0', '#1d48be', '#193ccc', '#1530db']
49
+ end
50
+ end
51
+
52
+ describe '#average_color' do
53
+ it 'return one average color' do
54
+ _(@palette.average_color.to_hex).must_equal '#318477'
55
+ end
56
+ end
57
+
58
+ describe '#light_colors' do
59
+ it 'extract light colors in provided limit' do
60
+ _(@palette.light_colors(40, 90).to_hex).must_equal ['#4dd915', '#1530db', '#49cc23', '#45c031', '#3da84d',
61
+ '#1d48be', '#399c5b', '#2154b0', '#193ccc', '#41b43f']
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+ require 'camalian'
7
+
8
+ describe Camalian::Quantization::Histogram do
9
+ describe 'palette with 15 colors extracted' do
10
+ before do
11
+ @image = Camalian.load(File.join(File.dirname(__FILE__), '../assets/palette.png'))
12
+ @palette = @image.prominent_colors(15, optimal: false, quantization: Camalian::QUANTIZATION_HISTOGRAM)
13
+ end
14
+
15
+ it 'must have 15 colors' do
16
+ _(@palette.size).must_equal 15
17
+ end
18
+
19
+ it 'sort similar colors in order' do
20
+ _(@palette.sort_similar_colors.map(&:to_hex)).must_equal PALLET_IMAGE_COLORS
21
+ end
22
+ end
23
+
24
+ it 'should extract distinct colors' do
25
+ colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
26
+ result = Camalian::Quantization::Histogram.new.process(colors, 3)
27
+
28
+ _(result.size).must_equal 3
29
+ _(result).must_equal colors
30
+ end
31
+
32
+ it 'should extract distinct colors lesser than pixels' do
33
+ colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
34
+ result = Camalian::Quantization::Histogram.new.process(colors, 2)
35
+
36
+ _(result.size).must_equal 2
37
+ end
38
+
39
+ it 'should extract distinct colors not more than pixels' do
40
+ colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
41
+ result = Camalian::Quantization::Histogram.new.process(colors, 4)
42
+
43
+ _(result.size).must_equal 3
44
+ end
45
+
46
+ it 'should extract same color' do
47
+ colors = %w[#FF0000 #FF0000 #FF0000].map { |c| Camalian::Color.from_hex(c) }
48
+ result = Camalian::Quantization::Histogram.new.process(colors, 3)
49
+
50
+ _(result.size).must_equal 1
51
+ _(result.to_hex).must_equal ['#ff0000']
52
+ end
53
+
54
+ it 'should extract multiple colors' do
55
+ colors = %w[#FF0000 #FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
56
+ result = Camalian::Quantization::Histogram.new.process(colors, 3)
57
+
58
+ _(result.size).must_equal 3
59
+ _(result.to_hex).must_equal %w[#ff0000 #00ff00 #0000ff]
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+ require 'camalian'
7
+
8
+ describe Camalian::Quantization::KMeans do
9
+ describe 'palette with 15 colors extracted' do
10
+ before do
11
+ @image = Camalian.load(File.join(File.dirname(__FILE__), '../assets/palette.png'))
12
+ @palette = @image.prominent_colors(15, optimal: false, quantization: Camalian::QUANTIZATION_K_MEANS)
13
+ end
14
+
15
+ it 'must have 15 colors' do
16
+ _(@palette.size).must_equal 15
17
+ end
18
+
19
+ it 'sort similar colors in order' do
20
+ _(@palette.sort_similar_colors.map(&:to_hex)).must_equal PALLET_IMAGE_COLORS
21
+ end
22
+ end
23
+
24
+ it 'should extract distinct colors' do
25
+ colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
26
+ result = Camalian::Quantization::KMeans.new.process(colors, 3)
27
+
28
+ _(result.size).must_equal 3
29
+ _(result).must_equal colors
30
+ end
31
+
32
+ it 'should extract distinct colors lesser than pixels' do
33
+ colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
34
+ result = Camalian::Quantization::KMeans.new.process(colors, 2)
35
+
36
+ _(result.size).must_equal 2
37
+ end
38
+
39
+ it 'should extract distinct colors not more than pixels' do
40
+ colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
41
+ result = Camalian::Quantization::KMeans.new.process(colors, 4)
42
+
43
+ _(result.size).must_equal 3
44
+ end
45
+
46
+ it 'should extract same color' do
47
+ colors = %w[#FF0000 #FF0000 #FF0000].map { |c| Camalian::Color.from_hex(c) }
48
+ result = Camalian::Quantization::KMeans.new.process(colors, 3)
49
+
50
+ _(result.size).must_equal 1
51
+ _(result.to_hex).must_equal ['#ff0000']
52
+ end
53
+
54
+ it 'should extract multiple colors' do
55
+ colors = %w[#FF0000 #FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
56
+ result = Camalian::Quantization::KMeans.new.process(colors, 3)
57
+
58
+ _(result.size).must_equal 3
59
+ _(result.to_hex).must_equal %w[#ff0000 #00ff00 #0000ff]
60
+ end
61
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+ require 'camalian'
7
+
8
+ describe Camalian::Quantization::MedianCut do
9
+ describe 'palette with 15 colors extracted' do
10
+ before do
11
+ @image = Camalian.load(File.join(File.dirname(__FILE__), '../assets/palette.png'))
12
+ @palette = @image.prominent_colors(15, optimal: false, quantization: Camalian::QUANTIZATION_MEDIAN_CUT)
13
+ end
14
+
15
+ it 'must have 15 colors' do
16
+ _(@palette.size).must_equal 15
17
+ end
18
+
19
+ it 'sort similar colors in order' do
20
+ _(@palette.sort_similar_colors.map(&:to_hex)).must_equal ['#4dd915', '#49cc23', '#45c031', '#41b43f', '#3da84d',
21
+ '#399c5b', '#359069', '#318478', '#2d7886', '#296c94',
22
+ '#2560a2', '#2154b0', '#1d48be', '#193ccc', '#1530db']
23
+ end
24
+ end
25
+
26
+ it 'should extract distinct colors' do
27
+ colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
28
+ result = Camalian::Quantization::MedianCut.new.process(colors, 3)
29
+
30
+ _(result.size).must_equal 3
31
+ _(result.to_hex.sort).must_equal colors.map(&:to_hex).sort
32
+ end
33
+
34
+ it 'should extract distinct colors lesser than pixels' do
35
+ colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
36
+ result = Camalian::Quantization::MedianCut.new.process(colors, 2)
37
+
38
+ _(result.size).must_equal 2
39
+ end
40
+
41
+ it 'should extract distinct colors not more than pixels' do
42
+ colors = %w[#FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
43
+ result = Camalian::Quantization::MedianCut.new.process(colors, 4)
44
+
45
+ _(result.size).must_equal 3
46
+ end
47
+
48
+ it 'should extract same color' do
49
+ colors = %w[#FF0000 #FF0000 #FF0000].map { |c| Camalian::Color.from_hex(c) }
50
+ result = Camalian::Quantization::MedianCut.new.process(colors, 3)
51
+
52
+ _(result.size).must_equal 1
53
+ _(result.to_hex).must_equal ['#ff0000']
54
+ end
55
+
56
+ it 'only: should extract multiple colors' do
57
+ colors = %w[#FF0000 #FF0000 #00FF00 #0000FF].map { |c| Camalian::Color.from_hex(c) }
58
+ result = Camalian::Quantization::MedianCut.new.process(colors, 3)
59
+
60
+ _(result.size).must_equal 3
61
+ _(result.to_hex).must_equal ['#ff0000', '#0000ff', '#00ff00']
62
+ end
63
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+ SimpleCov.start
5
+
6
+ require 'minitest/autorun'
7
+ require 'minitest/spec'
8
+ require 'camalian'
9
+
10
+ PALLET_IMAGE_COLORS = %w[#4dd915 #49cc23 #45c031 #41b43f #3da84d #399c5b #359069 #318478 #2d7886 #296c94 #2560a2
11
+ #2154b0 #1d48be #193ccc #1530db].freeze
metadata CHANGED
@@ -1,16 +1,75 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: camalian
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
5
- prerelease:
4
+ version: 0.2.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Nazar Hussain
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-09-12 00:00:00.000000000 Z
13
- dependencies: []
11
+ date: 2021-04-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: chunky_png
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.3.14
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.3.14
33
+ - !ruby/object:Gem::Dependency
34
+ name: minitest
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.14'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 5.14.2
43
+ type: :development
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '5.14'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 5.14.2
53
+ - !ruby/object:Gem::Dependency
54
+ name: rake
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '13.0'
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 13.0.1
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '13.0'
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 13.0.1
14
73
  description: Library used to deal with colors and images
15
74
  email:
16
75
  - nazarhussain@gmail.com
@@ -18,38 +77,63 @@ executables: []
18
77
  extensions: []
19
78
  extra_rdoc_files: []
20
79
  files:
21
- - .gitignore
80
+ - ".github/workflows/gpr-push.yml"
81
+ - ".github/workflows/ruby.yml"
82
+ - ".github/workflows/rubygems-push.yml"
83
+ - ".gitignore"
84
+ - ".overcommit.yml"
85
+ - ".rubocop.yml"
86
+ - ".tool-versions"
87
+ - ".travis.yml"
22
88
  - Gemfile
23
89
  - LICENSE.txt
24
90
  - README.md
25
91
  - Rakefile
26
92
  - camalian.gemspec
27
93
  - lib/camalian.rb
94
+ - lib/camalian/color.rb
95
+ - lib/camalian/image.rb
96
+ - lib/camalian/palette.rb
97
+ - lib/camalian/quantization/histogram.rb
98
+ - lib/camalian/quantization/k_means.rb
99
+ - lib/camalian/quantization/median_cut.rb
28
100
  - lib/camalian/version.rb
29
- homepage: http://www.nazarhussain.com
101
+ - test/assets/palette.png
102
+ - test/color_test.rb
103
+ - test/palette_test.rb
104
+ - test/quantization/histogram_test.rb
105
+ - test/quantization/k_means_test.rb
106
+ - test/quantization/median_cut_test.rb
107
+ - test/test_helper.rb
108
+ homepage: https://github.com/nazarhussain/camalian
30
109
  licenses:
31
110
  - MIT
111
+ metadata: {}
32
112
  post_install_message:
33
113
  rdoc_options: []
34
114
  require_paths:
35
115
  - lib
36
116
  required_ruby_version: !ruby/object:Gem::Requirement
37
- none: false
38
117
  requirements:
39
- - - ! '>='
118
+ - - ">="
40
119
  - !ruby/object:Gem::Version
41
- version: '0'
120
+ version: '2.4'
42
121
  required_rubygems_version: !ruby/object:Gem::Requirement
43
- none: false
44
122
  requirements:
45
- - - ! '>='
123
+ - - ">="
46
124
  - !ruby/object:Gem::Version
47
125
  version: '0'
48
126
  requirements: []
49
- rubyforge_project:
50
- rubygems_version: 1.8.23
127
+ rubygems_version: 3.0.3.1
51
128
  signing_key:
52
- specification_version: 3
129
+ specification_version: 4
53
130
  summary: Library used to deal with colors and images. You can extract colors from
54
131
  images.
55
- test_files: []
132
+ test_files:
133
+ - test/assets/palette.png
134
+ - test/color_test.rb
135
+ - test/palette_test.rb
136
+ - test/quantization/histogram_test.rb
137
+ - test/quantization/k_means_test.rb
138
+ - test/quantization/median_cut_test.rb
139
+ - test/test_helper.rb