camalian 0.0.3 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
- require 'rmagick'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Camalian
4
+ # Load image into Camalian
4
5
  class Image
5
6
  attr_accessor :src_file_path
6
7
 
@@ -8,29 +9,30 @@ module Camalian
8
9
  @src_file_path = file_path
9
10
  end
10
11
 
11
- def prominent_colors(count=Camalian.options[:color_count])
12
- image = ::Magick::Image.read(@src_file_path)[0]
13
- image.resize!(100,100)
14
- colors = Palette.new
15
- initial_count = count
16
- q = nil
17
- loop do
18
- q = image.quantize(initial_count, Magick::RGBColorspace)
19
- break if q.color_histogram.size > count
20
- initial_count = initial_count + 10
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
+ )
21
23
  end
22
- palette = q.color_histogram.sort {|a, b| b[1] <=> a[1]}
23
- (0..[count, palette.count].min - 1).each do |i|
24
- c = palette[i].first.to_s.split(',').map {|x| x[/\d+/]}
25
- c.pop
26
- c[0], c[1], c[2] = [c[0], c[1], c[2]].map { |s|
27
- s = s.to_i
28
- s = s / 256 if s / 256 > 0 # not all ImageMagicks are created equal....
29
- s
30
- }
31
- colors << Color.new(c[0],c[1],c[2])
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
32
33
  end
33
- return colors.uniq(&:to_hex)
34
+
35
+ palette
34
36
  end
35
37
  end
36
38
  end
@@ -1,27 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Camalian
4
+ # Collection of colors with some useful features
2
5
  class Palette < Array
6
+ def self.from_hex(hex_values)
7
+ new(hex_values.map { |v| Color.from_hex(v) })
8
+ end
3
9
 
4
10
  def sort_by_lightness
5
- Palette.new(self.sort_by { |a| a.l }.reverse)
11
+ Palette.new(sort_by(&:l).reverse)
6
12
  end
7
13
 
8
14
  def sort_by_hue
9
- Palette.new(self.sort_by { |a| a.h }.reverse)
15
+ Palette.new(sort_by(&:h).reverse)
10
16
  end
11
17
 
12
18
  def sort_similar_colors
13
- Palette.new(self.sort_by { |a| a.hsv })
19
+ Palette.new(sort_by(&:hsv))
14
20
  end
15
21
 
16
22
  def sort_by_saturation
17
- Palette.new(self.sort_by { |a| a.s }.reverse)
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)
18
33
  end
19
34
 
20
35
  def light_colors(limit1, limit2)
21
36
  min = [limit1, limit2].min
22
37
  max = [limit1, limit2].max
23
- table = self.dup
38
+ table = dup
24
39
  Palette.new(table.delete_if { |color| color.l > max or color.l < min })
25
40
  end
41
+
42
+ def to_hex
43
+ map(&:to_hex)
44
+ end
26
45
  end
27
46
  end
@@ -0,0 +1,29 @@
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
+ buckets = buckets.values
17
+ buckets = buckets.sort_by(&:size).reverse
18
+ average_colors = buckets.map(&:average_color)
19
+ Palette.new(average_colors[0...count])
20
+ end
21
+
22
+ private
23
+
24
+ def bucket_key(color, bucket_size)
25
+ "#{color.r / bucket_size}:#{color.g / bucket_size}:#{color.b / bucket_size}"
26
+ end
27
+ end
28
+ end
29
+ 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.3"
4
+ VERSION = '0.2.2'
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