camalian 0.0.3 → 0.2.2

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