Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit 6f101dd5aa5ea30d2fcfce860e480663ca265eb4 @quadule committed Aug 20, 2011
4 .gitignore
@@ -0,0 +1,4 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in colorscore.gemspec
+gemspec
7 Rakefile
@@ -0,0 +1,7 @@
+require "rake/testtask"
+require "bundler/gem_tasks"
+
+task :default => :test
+Rake::TestTask.new do |t|
+ t.test_files = FileList["test/test_helper.rb", "test/*_test.rb"]
+end
20 colorscore.gemspec
@@ -0,0 +1,20 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "colorscore/version"
+
+Gem::Specification.new do |s|
+ s.name = "colorscore"
+ s.version = Colorscore::VERSION
+ s.authors = ["Milo Winningham"]
+ s.email = ["milo@winningham.net"]
+ s.summary = %q{Finds the dominant colors in an image.}
+ s.description = %q{Finds the dominant colors in an image and scores them against a user-defined palette, using the CIE2000 Delta E formula.}
+
+ s.add_dependency "color"
+ s.add_development_dependency "rake"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end
6 lib/colorscore.rb
@@ -0,0 +1,6 @@
+require "color"
+
+require "colorscore/histogram"
+require "colorscore/metrics"
+require "colorscore/palette"
+require "colorscore/version"
24 lib/colorscore/histogram.rb
@@ -0,0 +1,24 @@
+module Colorscore
+ class Histogram
+ def initialize(image_path, colors=16, depth=8)
+ output = `convert #{image_path} -resize 400x400 -format %c -dither None -quantize LAB -colors #{colors} -depth #{depth} histogram:info:-`
+ @lines = output.lines.sort.reverse.map(&:strip).reject(&:empty?)
+ end
+
+ # Returns an array of colors in descending order of occurances.
+ def colors
+ hex_values = @lines.map { |line| line[/#[0-9A-F]+/] }
+ hex_values.map { |hex| Color::RGB.from_html(*hex) }
+ end
+
+ def color_counts
+ @lines.map { |line| line.split(':')[0].to_i }
+ end
+
+ def scores
+ total = color_counts.inject(:+).to_f
+ scores = color_counts.map { |count| count / total }
+ scores.zip(colors)
+ end
+ end
+end
141 lib/colorscore/metrics.rb
@@ -0,0 +1,141 @@
+module Colorscore
+ module Metrics
+ def self.similarity(a, b)
+ 1 - distance(a, b)
+ end
+
+ def self.distance(color_1, color_2)
+ l1, a1, b1 = xyz_to_lab(*rgb_to_xyz(color_1))
+ l2, a2, b2 = xyz_to_lab(*rgb_to_xyz(color_2))
+
+ distance = delta_e_cie_2000(l1, a1, b1, l2, a2, b2)
+ scale(distance, 0..100)
+ end
+
+ # Ported from colormath for Python.
+ def self.delta_e_cie_2000(l1, a1, b1, l2, a2, b2)
+ kl = kc = kh = 1
+
+ avg_lp = (l1 + l2) / 2.0
+ c1 = Math.sqrt((a1 ** 2) + (b1 ** 2))
+ c2 = Math.sqrt((a2 ** 2) + (b2 ** 2))
+ avg_c1_c2 = (c1 + c2) / 2.0
+
+ g = 0.5 * (1 - Math.sqrt((avg_c1_c2 ** 7.0) / ((avg_c1_c2 ** 7.0) + (25.0 ** 7.0))))
+
+ a1p = (1.0 + g) * a1
+ a2p = (1.0 + g) * a2
+ c1p = Math.sqrt((a1p ** 2) + (b1 ** 2))
+ c2p = Math.sqrt((a2p ** 2) + (b2 ** 2))
+ avg_c1p_c2p = (c1p + c2p) / 2.0
+
+ if degrees(Math.atan2(b1,a1p)) >= 0
+ h1p = degrees(Math.atan2(b1,a1p))
+ else
+ h1p = degrees(Math.atan2(b1,a1p)) + 360
+ end
+
+ if degrees(Math.atan2(b2,a2p)) >= 0
+ h2p = degrees(Math.atan2(b2,a2p))
+ else
+ h2p = degrees(Math.atan2(b2,a2p)) + 360
+ end
+
+ if (h1p - h2p).abs > 180
+ avg_hp = (h1p + h2p + 360) / 2.0
+ else
+ avg_hp = (h1p + h2p) / 2.0
+ end
+
+ t = 1 - 0.17 * Math.cos(radians(avg_hp - 30)) + 0.24 * Math.cos(radians(2 * avg_hp)) + 0.32 * Math.cos(radians(3 * avg_hp + 6)) - 0.2 * Math.cos(radians(4 * avg_hp - 63))
+
+ diff_h2p_h1p = h2p - h1p
+ if diff_h2p_h1p.abs <= 180
+ delta_hp = diff_h2p_h1p
+ elsif diff_h2p_h1p.abs > 180 && h2p <= h1p
+ delta_hp = diff_h2p_h1p + 360
+ else
+ delta_hp = diff_h2p_h1p - 360
+ end
+
+ delta_lp = l2 - l1
+ delta_cp = c2p - c1p
+ delta_hp = 2 * Math.sqrt(c2p * c1p) * Math.sin(radians(delta_hp) / 2.0)
+
+ s_l = 1 + ((0.015 * ((avg_lp - 50) ** 2)) / Math.sqrt(20 + ((avg_lp - 50) ** 2.0)))
+ s_c = 1 + 0.045 * avg_c1p_c2p
+ s_h = 1 + 0.015 * avg_c1p_c2p * t
+
+ delta_ro = 30 * Math.exp(-((((avg_hp - 275) / 25) ** 2.0)))
+ r_c = Math.sqrt(((avg_c1p_c2p ** 7.0)) / ((avg_c1p_c2p ** 7.0) + (25.0 ** 7.0)));
+ r_t = -2 * r_c * Math.sin(2 * radians(delta_ro))
+
+ delta_e = Math.sqrt(((delta_lp / (s_l * kl)) ** 2) + ((delta_cp / (s_c * kc)) ** 2) + ((delta_hp / (s_h * kh)) ** 2) + r_t * (delta_cp / (s_c * kc)) * (delta_hp / (s_h * kh)))
+ end
+
+ def self.rgb_to_xyz(color)
+ color = color.to_rgb
+ r, g, b = color.r, color.g, color.b
+
+ # assuming sRGB (D65)
+ r = (r <= 0.04045) ? r/12.92 : ((r+0.055)/1.055) ** 2.4
+ g = (g <= 0.04045) ? g/12.92 : ((g+0.055)/1.055) ** 2.4
+ b = (b <= 0.04045) ? b/12.92 : ((b+0.055)/1.055) ** 2.4
+
+ r *= 100
+ g *= 100
+ b *= 100
+
+ x = 0.412453*r + 0.357580*g + 0.180423*b
+ y = 0.212671*r + 0.715160*g + 0.072169*b
+ z = 0.019334*r + 0.119193*g + 0.950227*b
+
+ [x, y, z]
+ end
+
+ def self.xyz_to_lab(x, y, z)
+ x /= 95.047
+ y /= 100.000
+ z /= 108.883
+
+ if x > 0.008856
+ x = x ** (1.0/3)
+ else
+ x = (7.787 * x) + (16.0 / 116)
+ end
+
+ if y > 0.008856
+ y = y ** (1.0/3)
+ else
+ y = (7.787 * y) + (16.0 / 116)
+ end
+
+ if z > 0.008856
+ z = z ** (1.0/3)
+ else
+ z = (7.787 * z) + (16.0 / 116)
+ end
+
+ l = (116.0 * y) - 16.0
+ a = 500.0 * (x - y)
+ b = 200.0 * (y - z)
+
+ [l, a, b]
+ end
+
+ def self.scale(number, from_range, to_range=0..1, clamp=true)
+ if clamp && number <= from_range.begin
+ position = 0
+ elsif clamp && number >= from_range.end
+ position = 1
+ else
+ position = (number - from_range.begin).to_f / (from_range.end - from_range.begin)
+ end
+
+ position * (to_range.end - to_range.begin) + to_range.begin
+ end
+
+ def self.radians(degrees); degrees * Math::PI / 180; end
+ def self.degrees(radians); radians * 180 / Math::PI; end
+ end
+end
36 lib/colorscore/palette.rb
@@ -0,0 +1,36 @@
+module Colorscore
+ class Palette < Array
+ DEFAULT = ["660000", "990000", "cc0000", "cc3333", "ea4c88", "993399",
+ "663399", "333399", "0066cc", "0099cc", "66cccc", "77cc33",
+ "669900", "336600", "666600", "999900", "cccc33", "ffff00",
+ "ffcc33", "ff9900", "ff6600", "cc6633", "996633", "663300",
+ "000000", "999999", "cccccc", "ffffff"]
+
+ def self.default
+ new DEFAULT.map { |hex| Color::RGB.from_html(hex) }
+ end
+
+ def scores(histogram)
+ scores = map do |palette_color|
+ score = 0
+
+ histogram.scores.each_with_index do |item, index|
+ color_score, color = *item
+
+ color = color.to_hsl.tap { |c| c.s = 0.05 + c.s * (4 - c.l * 2.5) }.to_rgb
+
+ if (distance = Metrics.distance(palette_color, color)) < 0.275
+ distance_penalty = (1 - distance) ** 4
+ score += color_score * distance_penalty
+ end
+ end
+
+ [score, palette_color]
+ end
+
+ scores.reject { |score, color| score <= 0.05 }.
+ sort_by { |score, color| score }.
+ reverse
+ end
+ end
+end
3 lib/colorscore/version.rb
@@ -0,0 +1,3 @@
+module Colorscore
+ VERSION = "0.0.1"
+end
BIN test/fixtures/skydiver.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 test/histogram_test.rb
@@ -0,0 +1,12 @@
+require File.expand_path("../test_helper", __FILE__)
+
+class HistogramTest < Test::Unit::TestCase
+ def setup
+ @colors = 7
+ @histogram = Histogram.new("test/fixtures/skydiver.jpg", @colors)
+ end
+
+ def test_color_count_is_correct
+ assert_equal @colors, @histogram.colors.size
+ end
+end
13 test/metrics_test.rb
@@ -0,0 +1,13 @@
+require File.expand_path("../test_helper", __FILE__)
+
+class MetricsTest < Test::Unit::TestCase
+ def test_no_distance_between_identical_colors
+ color = Color::RGB.new(123, 45, 67)
+ assert_equal 0, Metrics.distance(color, color)
+ end
+
+ def test_maximum_similarity_between_identical_colors
+ color = Color::RGB.new(123, 45, 67)
+ assert_equal 1, Metrics.similarity(color, color)
+ end
+end
13 test/palette_test.rb
@@ -0,0 +1,13 @@
+require File.expand_path("../test_helper", __FILE__)
+
+class PaletteTest < Test::Unit::TestCase
+ def setup
+ @histogram = Histogram.new("test/fixtures/skydiver.jpg")
+ @palette = Palette.default
+ end
+
+ def test_skydiver_photo_is_mostly_blue
+ score, color = @palette.scores(@histogram).first
+ assert_equal Color::RGB.from_html('0099cc'), color
+ end
+end
3 test/test_helper.rb
@@ -0,0 +1,3 @@
+require "test/unit"
+require "colorscore"
+include Colorscore

0 comments on commit 6f101dd

Please sign in to comment.
Something went wrong with that request. Please try again.