Selective color with ChunkyPNG
Remember our last ChunkyPNG contest? We had a lot of entries, a lot of fun and a bunch of emails asking us to do it again, so here’s another one!
You’ve probably seen images with the selective color effect before, where the image is in black and white but there’s some element in there that’s colored. Some camera’s have this built in, but most are post-processed color pictures.
We’re not going to use fancy camera’s or expensive photo editing tools. We’re programmers, so we should be able to build a tool that does this for us.

Here’s the image we’ll be working with. Choose one color – bonus points if you choose the yellow or one of the blue ones – and create a black and white image with only one of the crayons in color using ChunkyPNG.
Remember, the crayons have a range of colors, they’re not just yellow, for example. It might be a good idea to check out some other color scheme than plain old RGB to support that.
Please put your entry in a Gist with the resulting image, like this example. Please don’t include the input image and don’t fork the example Gist. You can’t add images in the Gist web interface, so you’ll have to clone your Gist and add yout output image using git. If you don’t know how, just send me a message and I’ll help you out.
You have one week to get your entry in, so I’m sure you can make something awesome! Good luck!
Prize
This week’s winner can choose between $25 of 6sync hosting credit or one month of free Github Micro!
-
Finished in 1st place with a final score of 4.1/5. (View the Gist)README.md
How to get the selective color effect
When I started thinking about this Codebrawl, I had essentially two ideas how to do get the selective color effect: Try to detect each crayon as an object and keep one of them in color, or take a reference color and just keep the colors that are similar to it. The first idea would be more universal, but seems to me much harder to implement. I chose the second one, so I needed a way to measure the distance between to colors.
How to measure the distance between colors
There are several ways to get a value that tells you how much colors are like each other. If you have an RGB pixel, you could just take these 3 values as coordinates in a 3D space and calculate the distance there. I did not actually implement this, as it does not represent similar colors in the way we would see them. For example, the color (255,0,0) and (127,0,0) would both be just red, but they would have the same distance as (255, 128, 0), which introduces green to the color mix and looks pretty different.
This also tells us something about what kind of representation we want: One where only the color counts, but not the brightness (or luminance). I first looked at a color representation called rg chromaticty space.
rg chromacity
rg chromacity is a simple way to remove intensity information from your colors and only keep the proportions of red, green and blue. You just normalize the values of the r, g and b components to be between 0 and 1, with r + g + b always adding up to one. It is called rg chromacity because only the red and green components are needed to describe a color, as the blue component is always b = 1 - (r + g). For example, rgb(255,0,0) is rg(1,0), as is rgb(200,0,0).
You now have a 2d space, so distances can be easily calculated. This gives somewhat satisfactory results, at least for some of the crayons. But in the end, it just was not as good as I had hoped.
Part of this surely is grounded in how this color space actually looks. It is somewhat uneven, as the distance between pure red, rg(1,0) and pure blue, rg(0,0) is one. But between red and green,rg(1,0) and rg(0,1), it is the square root of two! By just measuring the distance, I am essentially cutting a circle of colors I want to keep, but similar colors in this space are not uniformly distributed.
I had a lot of ideas how to counter these problems: Make colors spread from a reference color if the neighbor are similar, make negative cuts in this space by specifying colors that should always be made gray, etc...
What I really needed was a better distance function for my colors. Let's look at HSV.
HSV
If you look at color spaces on Wikipedia, you will find HSL and HSV pretty soon. They each use the hue, saturation, and lightness or value to define a color. If you take a look at some pictures of this color space, you will quickly see that the hue component looks pretty much like what we need.
Now, calculating the hue is more complicated than calculating rg chromacity colors. If I understood it correctly, you make a hexagon, put red at 0°, green at 120° and blue at 240° and then calculate where your color lies. You can see the formula I used in the code. I did move the result around by a few degrees so pure red will be zero. I probably have a bug somewhere, but even if I would not correct it, it does not matter where red, green and blue lie exactly, as long as they have the right distance from each other.
Calculating the distance is just subtracting one hue from the other. We have to do it two times, calculating the modular distance, as hue is circular and has red on both ends. This gives good results for most colors. I could not get the yellow and red crayon to seperate perfectly... This may be because I did not find a good reference color, or just that the dark yellow in the tip of the crayon and the light red are actually too similar and this approach won't work at all. If other entries managed to get a perfect red crayon with no yellow using just the hue, you know which one it is ;-)
Better color distances
I tackled the problem of color similarity from a rather primitive point of view: Numerical values of single pixels. As it turns out, this is not enough to mirror human color perception. Take a look at this: http://gizmodo.com/5839481/the-most-wicked-optical-illusion-ive-seen-so-far. Both spirals have the same color, but you would never think that if you didn't know or check. As far as I know, no algorithm that works for cases like this exists, yet.
There is a standard for measuring color difference by the International Commission on Illumination (sounds good, right?), that takes into account how humans perceive color, so it should give you better results - if you are human, that is. I did not try it, though, as it seems somewhat complicated and I wanted to keep this entry short and to the point. Maybe you want to take a shot?
BONUS: Javascript version
I also wrote a javascript version of the same algorithm which you can use to quickly check the effects of changing the reference color or the color distance. You can find it at http://severe-autumn-9391.heroku.com/index.html.
Use the slider to set the color distance that will still be colored, and just click on any point in the image on the right to set it as reference. Have fun!
output.png
extensions.rbmodule ChunkyPNG::Color # See http://en.wikipedia.org/wiki/Hue#Computing_hue_from_RGB def self.hue(pixel) r, g, b = r(pixel), g(pixel), b(pixel) return 0 if r == b and b == g ((180 / Math::PI * Math.atan2((2 * r) - g - b, Math.sqrt(3) * (g - b))) - 90) % 360 end # The modular distance, as the hue is circular def self.distance(pixel, poxel) hue_pixel, hue_poxel = hue(pixel), hue(poxel) [(hue_pixel - hue_poxel) % 360, (hue_poxel - hue_pixel) % 360].min end end module SelectiveColor # Really simple, just change the pixels to grayscale if their distance to a # reference hue is larger than a delta value. def to_selective_color!(reference, delta) pixels.map!{|pixel| ChunkyPNG::Color.distance(pixel, reference) > delta ? ChunkyPNG::Color.to_grayscale(pixel) : pixel} self end end
selective_color.rbView full entryrequire "chunky_png" require "./extensions.rb" image = ChunkyPNG::Image.from_file("input.png") image.extend(SelectiveColor) # Try the other colors if you like! # keep = ChunkyPNG::Color.rgb(221,57,42) # keep = ChunkyPNG::Color.rgb(152,216,56) keep = ChunkyPNG::Color.rgb(0,125,209) image.to_selective_color!(keep, 15) image.save("output.png")
-
Finished in 2nd place with a final score of 3.8/5. (View the Gist)README.md
Singucolor
Singular was written for this Code Brawl and is a small library for filtering selective colors with ChunkyPNG. An example of using the library by processing input.png and outputing five images each with a different crayon selectively colored is included in singular.rb. Also included are a few utilities for calculating the hue of a given RGB color, drawing a Gruff histogram, and a primitive engine for guessing which hues the user might be interested in filtering. Ideally the user would use their favorite photo editor to select the desired color. In an attempt to appeal to programmers, this example is also capable of guessing which colors the user might be interested in filtering.
All code was written for Code Brawl. Besides chunky_png the only other dependencies are Ruby 1.9.2 and Gruff. Gruff is used only for drawing the hue chart.
Designed for Ruby 1.9.2 stable
How to run the example
- download or clone the gist and copy input.png
- rvm 1.9.2 # or otherwise ensure you are running Ruby 1.9.2
- gem install chunky_png gruff
- ruby singucolor.rb
The Fun Part
Skipping ahead to the fun part, here is how the library is used to filter each crayon...
crayons = [ { :name => 'green', :color => 'a8e060', :range => 20 }, { :name => 'yellow', :color => 'de8800', :range => 20 }, { :name => 'blue', :color => '0061af', :range => 20 }, { :name => 'purple', :color => '4c4bae', :range => 20 }, { :name => 'red', :color => '650100', :range => 12 } ] crayons.each do |crayon| puts "Processing #{crayon[:name]} crayon..." ColorFilter.new('input.png') .color(crayon[:color]) .range(crayon[:range]) .write("output-#{crayon[:name]}-crayon.png") endThe Guts
There are three classes in the Singucolor library: ColorFilter, ColorAnalyzer, and MultiFormatColor.
ColorFilter accepts a png filename, a color or hue, and an optional hue range and writes a filtered image to a new png file. ColorFilter uses MultiFormatColor to calculate the desired hue and input hues. For each input pixel a hue is calculated. If the difference between the input hue and desired hue exceeds the specified range (or default of 20 degrees) then a grayscale pixel is written in its place, otherwise the original pixel is written.
MultiFormatColor represents a color and it's associated hue and integer value. It delegates most operations, except for hue calculation, to ChunkyPNG::Color for the sake of convenience.
ColorAnalyzer processes an image and tries to determine which hues are the most popular. It also leverages MultiFormatColor for processing color information. Using Gruff it can also generate a graph of hue vs pixel quantity.
output-red-crayon.png
output-purple-crayon.png
hue_chart.png
output-green-crayon.png
output-blue-crayon.png
output-yellow-crayon.png
color_filter.rbrequire 'rubygems' require 'chunky_png' require 'multi_format_color' class ColorFilter def initialize(filename) @png = ChunkyPNG::Image.from_file(filename) @range = 20 end def color(color_as_hex) @hue = MultiFormatColor.from_hex(color_as_hex).hue self end def hue(hue_in_degrees) @hue = hue_in_degrees self end def range(range_in_degrees) @range = range_in_degrees self end def write(filename) for w in (0..@png.width-1) for h in (0..@png.height-1) @png.set_pixel(w, h, filter_pixel(@png[w,h])) end end @png.save(filename) end private def filter_pixel(input_pixel) input_color = MultiFormatColor.new(input_pixel) if (input_color.hue - @hue).abs > @range input_color.to_grayscale else input_pixel end end end
multi_format_color.rbrequire 'rubygems' require 'chunky_png' class MultiFormatColor def initialize(color_as_integer) @red = ChunkyPNG::Color.r(color_as_integer) @green = ChunkyPNG::Color.g(color_as_integer) @blue = ChunkyPNG::Color.b(color_as_integer) @integer_value = color_as_integer end def self.from_hex(color_as_hex) self.new(ChunkyPNG::Color.from_hex(color_as_hex)) end def hue @hue ||= Math.atan2(2*@red-@green-@blue+0.001, Math.sqrt(3)*(@green-@blue+0.001)) * 180 / Math::PI end def to_grayscale ChunkyPNG::Color.to_grayscale(@integer_value) end end
singucolor.rb#!/usr/bin/ruby $LOAD_PATH << './lib' require 'rubygems' require 'chunky_png' require 'color_filter' require 'color_analyzer' # SPECIFIY EACH CRAYON COLOR AND LET SINGUCOLOR DO THE REST # Use your favorite photo editor to grab the color of each crayon. # Range defaults to 20 but you can override for more control over bleeding. crayons = [ { :name => 'green', :color => 'a8e060', :range => 20 }, { :name => 'yellow', :color => 'de8800', :range => 20 }, { :name => 'blue', :color => '0061af', :range => 20 }, { :name => 'purple', :color => '4c4bae', :range => 20 }, { :name => 'red', :color => '650100', :range => 12 } ] crayons.each do |crayon| puts "Processing #{crayon[:name]} crayon..." ColorFilter.new('input.png') .color(crayon[:color]) .range(crayon[:range]) .write("output-#{crayon[:name]}-crayon.png") end # ASK SINGUCOLOR FOR SOME HINTS puts "My best guess of the top five hues:" analyzer = ColorAnalyzer.new('input.png') analyzer.chart('hue_chart.png') analyzer.top(5).each_peak_hue do |peak_hue| puts " - #{peak_hue} degrees" # Uncomment the following line if you want to render the guesses # ColorFilter.new('input.png').hue(peak_hue).range(20).write("output-auto-#{peak_hue}.png") end
color_analyzer.rbView full entryrequire 'rubygems' require 'chunky_png' require 'gruff' require 'multi_format_color' class ColorAnalyzer def initialize(filename) @png = ChunkyPNG::Image.from_file(filename) @top = 5 end def top(count) @top = count @max_hues = nil self end def chart(filename) load_data g = Gruff::Line.new g.title = "Hue Chart" g.labels = { 0 => "0", 180 => "180", 359 => "359" } g.data("Pixels", @raw_hues) g.x_axis_label = "Hue (degrees)" g.y_axis_label = "Quantity" g.write filename self end def each_peak_hue load_data @max_hues.each do |max_hue| yield max_hue[:hue] end end private def load_data if @raw_hues.nil? # Collect stats on the hues of each pixel @raw_hues = Array.new(360) { 0 } @png.pixels.each do |pixel| @raw_hues[MultiFormatColor.new(pixel).hue.round] += 1 end end if @max_hues.nil? # Determine the 5 most popular hues @max_hues = Array.new(@top) { { :hue => 0, :quantity => 0 } } max_quantity = @png.height * @png.width @max_hues.each do |max_hue| max_hue[:hue] = max_below(@raw_hues, max_quantity) max_hue[:quantity] = @raw_hues[max_hue[:hue]] max_quantity = max_hue[:quantity] end end end def max_below(ary, bound) max_value = 0 max_index = 0 ary.each_with_index do |value, index| if value > max_value && value < bound max_index = index max_value = value end end max_index end end
-
Finished in 3rd place with a final score of 3.6/5. (View the Gist)README.md
Colr.rb
Colr selects colors based on hue calculations. It knows nothing about the image before initialization and works with any valid HTML color: red, green, aqua, brown, etc.
Colr allows you to adjust the tolerance and hue settings so you can select colors from any image!
Example:
@colr = Colr.new('input.png') @colr.select(:red).save("red.png") # To select all the colors: Colr.new('input.png').select(:yellow, 40, -40).save("yellow.png") Colr.new('input.png').select(:red).save("red.png") Colr.new('input.png').select(:green, 30, -60).save("green.png") Colr.new('input.png').select(:blue, 20, -50).save("blue.png") Colr.new('input.png').select(:violet, 20, -70).save("violet.png")
violet.png
blue.png
yellow.png
red.png
green.png
test_colr.rbrequire 'minitest/autorun' require File.join(File.dirname(__FILE__), 'colr.rb') describe Colr do before do @colr = Colr.new('input.png') end it 'calculates hues for html colors' do @colr.hue(0xff000000).must_equal 0 end it "returns a range for positive values" do @colr.swatch(:red, 10, 0).must_equal 0..10 end it 'returns values for negative degrees' do @colr.swatch(:green, 30, -60).must_equal 60..90 end end
colr.rbView full entryrequire 'chunky_png' class Colr include ChunkyPNG::Color def initialize(png) @input = ChunkyPNG::Image.from_file(png) end # Converts each pixel of the image to grayscale except the given color. You # can use the tolerance and degrees (hues) to tune Colr to match any image! # # Example: # @colr.select(:red) #=> ChunkyPNG::Image # # Returns the resulting image def select(color, tolerance = 10, degrees = 0) hues = swatch(color, tolerance, degrees) @input.pixels.map! { |c| hues.include?( hue(c) ) ? c : to_grayscale(c) } @input end # Calculates a hue of the given color # Algorithm from http://en.wikipedia.org/wiki/HSL_and_HSV # # Example: # @colr.hue(0xff000000) #=> 0 # # Returns the resulting integer value of the hue def hue(color) r,g,b = to_truecolor_bytes(color) min, max = [r,g,b].min, [r,g,b].max d = max - min h = case max when min; 0 when r; 60 * (g-b)/d when g; 60 * (b-r)/d + 120 when b; 60 * (r-g)/d + 240 end % 360 end # Calculates a range to hues given the color, tolerance, and # # Example: # @colr.swatch(:red) #=> 0..10 # # Returns a new Range of hues def swatch(color, tolerance, degrees) h = hue(PREDEFINED_COLORS[color.to_sym]) + degrees Range.new(h, h+tolerance) end end
-
Finished in 4th place with a final score of 3.5/5. (View the Gist)README.md
Description
A new
ChunkyPNG::Image#select!method requires 3 parameters (x,yandtolerance), picks a pixel from the position (x,y) and decolorizes only those pixels of the image, which have different hue from the target pixel.toleranceparameter is the tolerance level to use when colorizing parts of image. Increase this level if too many pixels grayed out, decrease it if too many left colorized.Usage
image = ChunkyPNG::Image.from_file('input.png') image.select!(100, 101, 15.0) image.save("output.png")output_yellow.png
output_green.png
output_blue.png
input.png
output_dark_blue.png
output_red.png
selective_color.rbView full entryrequire 'chunky_png' module ChunkyPNG class Image def select!(x, y, tolerance) target_hue = hue(self[x, y]) for i in 0...width for j in 0...height rgb = self[i, j] if (target_hue - hue(rgb)).abs >= tolerance self[i, j] = to_gray(rgb) end end end end def to_gray(rgb) r = Color::r(rgb) g = Color::g(rgb) b = Color::b(rgb) gray = (r*0.30 + g*0.59 + b*0.11).round.to_i ChunkyPNG::Color(gray, gray, gray) end def hue(rgb) r = Color::r(rgb).to_f g = Color::g(rgb).to_f b = Color::b(rgb).to_f mx = [r, g, b].max mn = [r, g, b].min c = mx - mn 60 * if c == 0 0 elsif mx == g (b - r) / c + 2 elsif mx == b (r - g) / c + 4 elsif g < b (g - b) / c + 6 else (g - b) / c end end end end { :dark_blue => [ 60, 60, 10.0], :blue => [ 80, 80, 18.0], :green => [100, 100, 18.0], :yellow => [130, 135, 22.0], :red => [150, 160, 11.0] }.each do |crayon, (x, y, tolerance)| image = ChunkyPNG::Image.from_file('input.png') image.select!(x, y, tolerance) image.save("output_#{crayon}.png") end
-
Finished in 5th place with a final score of 3.5/5. (View the Gist)red.png
blue.png
violet.png
green.png
yellow.png
00-hue.rbView full entryrequire 'chunky_png' extend ChunkyPNG::Color # So you didn't like yellow, yes? Me neither. colors = { red: 0.00..0.04, yellow: 0.04..0.18, green: 0.18..0.33, blue: 0.34..0.60, violet: 0.61..1.00 } # Returns the hue from 0..1.00. (0.00 = red, 0.33 = green, 0.66 = blue) def h(c) rgb = [r(c), g(c), b(c)] min, mid, max = rgb.sort hue = 0.3333 * rgb.index(max) # Start with the brightest color. Then, inf = 0.1666 * (mid-min) / (max-min) # influence it with the middle color. inf *= -1 if rgb.index(mid) == (rgb.index(max)-1)%3 hue + inf end # ALL THE COLORS! colors.each do |color, range| image = ChunkyPNG::Image.from_file('input.png') image.pixels.map! { |c| range.include?(h c) ? c : to_grayscale(c) } image.save "#{color}.png" end # BONUS! To turn `h` into `hsb` (hue-sat-brightness), change line 20 to: # [ hue+inf, (max-min)*1.0/max, max/255.0 ]
-
Finished in 6th place with a final score of 3.4/5. (View the Gist)Gemfile
source "http://rubygems.org" gem "chunky_png", "~> 1.2.1" gem "colour", "~> 0.4.0"
.gitignoreinput.png /Gemfile.lock
readme.mdselective-color
Turns the input image (input.png) into a selectively colored image, turning it into grayscale with just the lighter blue crayon left colored.
dependencies
usage
$ bundle install $ curl -o input.png https://raw.github.com/gist/7addc24b123aad374832/96656b5a75287f8ca8beac523f1d32eee4030aa5/output.png $ ruby selective_color.rboutput.png
selective_color.rbView full entryrequire 'chunky_png' require 'colour' #color_range = (14..65) # yellow color_range = (170..220) # lighter blue input = ChunkyPNG::Image.from_file('input.png') output = input.grayscale input.pixels.each_index do |x| pixel = input.pixels[x] hsv = RGB.new(ChunkyPNG::Color.r(pixel), ChunkyPNG::Color.g(pixel), ChunkyPNG::Color.b(pixel)).to_hsv output.pixels[x] = pixel if color_range.cover? hsv.h end output.save('output.png')
-
Finished in 7th place with a final score of 3.3/5. (View the Gist)output.png
decolour.rbView full entryrequire 'chunky_png' module ChunkyPNG module Color def nearly(c, t) (hue(c) - hue(t)).abs < 10 end def hue(c) 180/Math::PI*Math.atan2(Math.sqrt(3)*(g(c)-b(c)) , 2*(r(c)-g(c)-b(c)) ) end end class Image def decolour! target = Color.rgba(235,73,54,255) target = Color.rgba(21,138,209,255) width.times do |n| height.times do |m| current = self[n,m] self[n,m] = Color.nearly(current, target)? current : Color.to_grayscale(current) end end end end end image = ChunkyPNG::Image.from_file('input.png') image.decolour! image.save('output.png')
-
Finished in 8th place with a final score of 3.2/5. (View the Gist)Gemfile
# A sample Gemfile source "http://rubygems.org" gem "chunky_png" # gem "rails"
Gemfile.lockGEM remote: http://rubygems.org/ specs: chunky_png (1.2.4) PLATFORMS ruby DEPENDENCIES chunky_pngREADME.mdCodebrawl Selective Color entry
All the code so far is in
select.rb. I set up a few helper functions at the top of the file to get color components in floating point for the hue calculations. The hue calculation itself is ripped straight from Wikipedia HSL and HSV, implemented in pure Ruby. I also took advantage of ChunkyPNG's already-implemented grayscale conversion (after rolling my own first, then realizing my mistake).The commands used to generate all the versions are:
./select.rb 20 65 input.png yellow.png ./select.rb 0 18 input.png red.png ./select.rb 65 90 input.png green.png ./select.rb 185 215 input.png blue.png ./select.rb 235 245 input.png violet.pngwhere the command line syntax is:
select.rb <lower hue limit, 0-360.0> <upper hue limit, 0-360.0> <input file> <output file>Since I'm not a GUI expert, I haven't wrapped this up with its own color picker - I used the ColorSync Utility built-in to my MacBook to estimate Hue value ranges for the different crayons.
yellow.png
violet.png
blue.png
red.png
green.png
select.rbView full entry#!/usr/bin/env ruby require 'bundler' require 'chunky_png' def r_f(color) ChunkyPNG::Color.r(color) / 255.0 end def g_f(color) ChunkyPNG::Color.g(color) / 255.0 end def b_f(color) ChunkyPNG::Color.b(color) / 255.0 end def hue(color) return 0.0 if ChunkyPNG::Color.grayscale? color alpha = 0.5 * (2*r_f(color) - g_f(color) - b_f(color)) beta = Math.sqrt(3)/2.0 * (g_f(color) - b_f(color)) rad = Math.atan2(beta, alpha) rad = 2*Math::PI + rad if rad < 0 rad / (2*Math::PI) * 360.0 end unless ARGV.length == 4 puts <<HELP Usage: select.rb <lower hue limit, 0-360.0> <upper hue limit, 0-360.0> <input file> <output file> HELP exit(0) end lower_limit = Float(ARGV[0]) upper_limit = Float(ARGV[1]) input_file = ARGV[2] output_file = ARGV[3] puts "reading from #{input_file}" puts "writing to #{output_file}" input = ChunkyPNG::Image.from_file(input_file) puts "keeping pixels with #{lower_limit} <= hue <= #{upper_limit}" (0...input.height).each do |y| puts "row #{y}" (0...input.width).each do |x| h = hue input[x,y] unless lower_limit <= h && h <= upper_limit input[x,y] = ChunkyPNG::Color.grayscale ChunkyPNG::Color.grayscale_teint input[x,y] end end end input.save(output_file)
-
Finished in 9th place with a final score of 3.2/5. (View the Gist)README.md
Selective color effect
This script applies the widely-known selective color effect on a picture.
It's run by executing
ruby selective_color.rbon the shell.The script will generate an
output.pngin the same folder as the script based on the source pictureinput.png.Pre-requisites
You need the gem
chunky_pnginstalled on your environment. Just run this:gem install chunky_pngOverrides
With the attached example
input.pngyou can modify the script to extract the color of a specific crayon (default is yellow).Just change line #6 with the color of the crayon you want.
output.png
input.png
chunky_png.rbrequire 'chunky_png' module ChunkyPNG class Image def [](x,y) Pixel.new(x, y, get_pixel(x,y), self) end end class Pixel def initialize(x,y,pixel,image) @x, @y, @pixel, @image = x, y, pixel, image end # Gets the hue of a color def hue r, g, b = rgb = Color.to_truecolor_bytes(@pixel).map{|v|v/255.0} min, max = rgb.min, rgb.max { r => (g-b)/(r-min)+0, g => (b-r)/(g-min)+2, b => (r-g)/(b-min)+4 }[max] / 6 end # Sets the pixel to a grayscaled color def make_gray @image.set_pixel(@x, @y, Color.to_grayscale(@pixel)) end end end
selective_color.rbView full entryrequire_relative 'chunky_png' rainbow = [0.000, 0.037, 0.185, 0.360, 0.600, 0.725] h = [:red, :yellow, :green, :blue, :purple] color = :yellow t = rainbow[h.index(color), 2] the_hues_i_want = (t[0]..t[1]) image = ChunkyPNG::Image.from_file('input.png') image.width.times {|x| image.height.times {|y| pixel = image[x,y] pixel.make_gray unless the_hues_i_want.include? pixel.hue } } image.save('output.png')
-
Finished in 10th place with a final score of 3.0/5. (View the Gist).gitignore
input.png
README.mkdSelective Desaturation ======================= It's not perfect, but this solution uses [Delta-E][] color differerence formula from the [Lab color space][LAB] (specifically the [CIE76][] definition) to compare every pixel in the image to the target color. If the difference is greater than some threshold, the pixels is set to grayscale. Thanks for [Bruce Lindbloom][Conversions] for the correct formulas, although I'm pretty sure they are exactly correct. It seems to work well enough, so it might just be the scale that is off. ## Example image = ChunkyPNG::Image.from_file('input.png') green = ChunkyPNG::Color.rgb(151, 207, 63) # this method takes an optional threshold paramter - 3.5 works for this image.selective_color!(green) image.save('output.png') [Conversions]: http://www.brucelindbloom.com/ [CIE76]: http://en.wikipedia.org/wiki/Color_difference#CIE76 [Delta-E]: http://www.colorwiki.com/wiki/Delta_E:_The_Color_Difference [LAB]: http://en.wikipedia.org/wiki/Lab_color_spaceoutput.png
solution.rbView full entryrequire 'oily_png' require 'matrix' module ChunkyPNG class Image def selective_color!(color, threshold=3.5) scores = [] pixels.map! do |pixel| score = ChunkyPNG::Color.lab_difference(pixel, color) scores << score if score > threshold ChunkyPNG::Color.to_grayscale(pixel) else pixel end end puts "Difference ranges from #{scores.min} -> #{scores.max}" end end module Color SRGB_GAMMA = 2.2 # http://www.brucelindbloom.com/ SRGB_MATRIX = ::Matrix[[0.4124564, 0.3575761, 0.1804375], [0.2126729, 0.7151522, 0.0721750], [0.0193339, 0.1191920, 0.9503041]] K = 24389.0/27.0 E = 216.0/24389.0 # http://en.wikipedia.org/wiki/Illuminant_D65 REFERENCE_WHITE_D65 = [95.047, 100.00, 108.883] # Calculate Delta E difference score # http://en.wikipedia.org/wiki/Color_difference#Delta_E def lab_difference(color, other_color) lab_color = rgb_to_lab(color) other_lab_color = rgb_to_lab(other_color) score = (0..2).collect do |i| (lab_color[i] - other_lab_color[i]) ** 2 end.reduce(:+) score = Math.sqrt(score) score end def rgb_to_lab(color) xyz_to_lab(rgb_to_xyz(color)) end def rgb_to_xyz(color) linearized = linearize(color) result = SRGB_MATRIX * linearized result.column_vectors[0].to_a end def xyz_to_lab(channels) lab = channels.collect.with_index do |channel, i| xyz_channel_to_lab_partial(channel / REFERENCE_WHITE_D65[i]) end l = 116 * lab[0] - 16 a = 500 * (lab[0] - lab[1]) b = 200 * (lab[1] - lab[2]) [l, a, b] end def xyz_channel_to_lab_partial(x) if x > E x ** 1/3.0 else (K * x + 16) / 116 end end def linearize(color) ::Matrix[[linearize_component(r(color))], [linearize_component(g(color))], [linearize_component(b(color))]] end def linearize_component(component) component / 255.0 end end end image = ChunkyPNG::Image.from_file('input.png') purple = ChunkyPNG::Color.rgb(57, 54, 166) blue = ChunkyPNG::Color.rgb(7, 127, 87) green = ChunkyPNG::Color.rgb(151, 207, 63) yellow = ChunkyPNG::Color.rgb(255, 214, 20) red = ChunkyPNG::Color.rgb(229, 41, 18) # 50, 68, 59 in LAB # 34, 19, 2 in XYZ image.selective_color!(green) image.save('output.png')
-
Finished in 11th place with a final score of 3.0/5. (View the Gist)output_5.png
output_3.png
output_1.png
output_2.png
output_4.png
gistfile1.rbView full entryrequire 'chunky_png' module ChunkyPNG::Color LUMA_WEIGHTS = [0.30, 0.59, 0.11] #based on http://en.wikipedia.org/wiki/HSL_and_HSV def self.to_hcy_floats(i) #creates hue, chroma, luma tuple r,g,b = to_truecolor_bytes(i).map {|x| x/255.0} minx,maxx = [r,g,b].minmax chroma = maxx-minx hue = if chroma == 0 0 else case maxx when r ((g-b)/chroma) % 6 when g (b-r)/chroma + 2 when b (r-g)/chroma + 4 end * 60 end luma = [r,g,b].zip(LUMA_WEIGHTS).map {|x,w| x*w}.inject(&:+) [hue,chroma,luma] end def self.hcy(hue,chroma,luma) segment = hue / 60 h2 = (1 - ((segment % 2) - 1).abs) #h2 is the portion of hue from the second-largest component. it gives us the distance from the 'max' component, along a hexagon edge, to the actual hue angle. is a float in 0..1 range. rgb1 = [ [1,h2,0], [h2,1,0], [0,1,h2], [0,h2,1], [h2,0,1], [1,0,h2] ][segment.floor].map {|x| chroma*x } m = luma - rgb1.zip(LUMA_WEIGHTS).map {|x,w| x*w}.inject(&:+) r,g,b = rgb1.map {|x| (x+m)}.map {|x| (x*255).to_i} rgb(r,g,b) end end if __FILE__ == $0 #unless we're being loaded as a library #TODO: get chunks and prompt colors = [0,36,75,205,240] tolerance = 19 colors.each_with_index do |color,i| image = ChunkyPNG::Image.from_file("input.png") image.pixels.map! do |p| h,c,y = ChunkyPNG::Color.to_hcy_floats(p) if (h-color).abs < tolerance || (h-color+360).abs < tolerance p else ChunkyPNG::Color.hcy(h,0,y) end end image.save("output_#{i+1}.png") end end
-
Finished in 12th place with a final score of 3.0/5. (View the Gist)output.png
solution.rbView full entry#!/usr/bin/env ruby require 'chunky_png' # Convert the RGB coordinates to polar form (HSV) and get the hue. # Formula from: http://en.wikipedia.org/wiki/Hue#Computing_hue_from_RGB # Doesn't handle all the cases (R >= B > G, for example), but it gets the job done. def get_hue(r,g,b) max = [r,g,b].max if max == r hue = 0.0 + 60.0 * (g - b) if(hue < 0.0) hue += 360.0 end elsif max == g hue = 120.0 + 60.0 * (b-r) else hue= 240+60*(r-g) end return hue end image = ChunkyPNG::Image.from_file('input.png') # The hue of the color we want to select color = 204 req_range = color-34..color+34 # Iterate over the pixels in the image. # If the hue for the pixel is NOT in the required range, convert that pixel to grayscale. # The required range is 20 degrees +/- a single hue. # Here blue has hue of 240, so our range is 220 -> 260. for i in 0...image.width for j in 0...image.height pixel = image.get_pixel(i,j) r = ChunkyPNG::Color.r(pixel).to_f g = ChunkyPNG::Color.g(pixel).to_f b = ChunkyPNG::Color.b(pixel).to_f hue = get_hue(r/255,g/255,b/255) # Convert co-ordinates to be between 0 and 1 if not req_range.include?(hue) image.set_pixel(i,j,ChunkyPNG::Color.to_grayscale(pixel)) end end end image.save('output.png')
-
Finished in 13th place with a final score of 3.0/5. (View the Gist)README.md
Selective Color
The algorithm calculates the hue of each pixel. Each hue interval represents a color hue, so knowing the intervals allow us to filter out hues we do not want (to change them to grayscale).
output_violet.png
selective.rbView full entryrequire 'chunky_png' include ChunkyPNG def hue pixel rgb = { r: Color.r(pixel), g: Color.g(pixel), b: Color.b(pixel) } y = 2 * rgb[:r] - rgb[:g] - rgb[:b] return 0 if y.zero? x = Math.sqrt(3) * (rgb[:g] - rgb[:b]) (57.296 * Math.atan2(y, x)).to_i + 180 end def hue_pass(hue, color) case color when :violet then (hue >= 0 and hue <= 35) else true end end def selective_color(input, output, color) image = Image.from_file input image.pixels.map! do |pixel| unless hue_pass hue(pixel), color Color.to_grayscale pixel else pixel end end image.save output end selective_color 'input.png', 'output_violet.png', :violet
-
Finished in 14th place with a final score of 2.8/5. (View the Gist)README.md
This is an entry to CodeBrawl Contest Selective color with ChunkyPNG
- Here I used a struct to store the rgba info (although I could've just used rgb instead).
- Added to_rgba method to Fixnum.
- Created an iterator for canvas that passes the rgba value to the block and assigns the result to the pixel at that point.
- Indigo Color Detection determined through brute force, deducing the needed conditions using any dropper/color metering tool on the image.
output.png
scolor.rbView full entryrequire 'chunky_png' class Color < Struct.new( :red, :green, :blue, :alpha) def to_i ChunkyPNG::Color.rgba(*self) end end class Fixnum def to_rgba Color[ChunkyPNG::Color.r(self), ChunkyPNG::Color.g(self), ChunkyPNG::Color.b(self), ChunkyPNG::Color.a(self)] end end class ChunkyPNG::Canvas def diff!(canvas, &block) area.times do |i| @pixels[i] = block.call(self.pixels[i].to_rgba, canvas.pixels[i].to_rgba) end end end image = ChunkyPNG::Canvas.from_file('input.png') gs_image = image.grayscale image.diff!(gs_image) do |original, grayscale| if(original.red < 120 && original.green < 120 && original.blue > 40 && original.blue - original.red > 40 && original.blue - original.green > 40 && (original.red - original.green).abs <= 10) original.to_i else grayscale.to_i end end image.save('output.png')
-
Finished in 15th place with a final score of 2.8/5. (View the Gist)output.png
selective_color.rbView full entryrequire 'chunky_png' module ChunkyPNG::Color def rgb_to_hsl(color) r = r(color) / 255.0 # 0..255 -> 0.0..1.0 g = g(color) / 255.0 b = b(color) / 255.0 max = [r, g, b].max min = [r, g, b].min l = (max + min) / 2.0 d = max - min if d == 0 h = s = 0 else if ( l < 0.5 ) s = d / ( max + min ) else s = d / ( 2.0 - max - min ) end d_r = (((max - r) / 6.0) + (max / 2.0)) / d d_g = (((max - g) / 6.0) + (max / 2.0)) / d d_b = (((max - b) / 6.0) + (max / 2.0)) / d if max == r h = d_b - d_g elsif max == g h = (1/3.0) + d_r - d_b else h = (2/3.0) + d_g - d_r end h += 1.0 if h < 0.0 h -= 1.0 if h > 1.0 end [h * 360, s * 100, l * 100].map(&:to_i) end end class ChunkyPNG::Canvas # delta between two angles in range (0..360) # e.g. smallest_delta_degree 5, 1 # => 4 # smallest_delta_degree 5, 359 # => 6 def smallest_delta_degree deg1, deg2 [deg1 - deg2, deg1 + 360 - deg2, deg2 - deg1 + 360].map(&:abs).min end # checks if a degree is in a degree range; handles special wrap around 360째 case def in_degree_range deg_start, deg_end, deg (deg_start > deg_end) ? (deg >= deg_start || deg <= deg_end) : (deg >= deg_start && deg <= deg_end) end def ajust_alpha color, alpha ChunkyPNG::Color.rgba(ChunkyPNG::Color.r(color), ChunkyPNG::Color.g(color), ChunkyPNG::Color.b(color), ChunkyPNG::Color.a(color) * alpha) end # Clears out (sets to transparent) all pixels that are in a specific hue range. # The hue values need to be ordered (but wrapping around the 360째 break is ok). # # Uses a linear ramp to soften the edges: # + If the hue of a pixel is between hue_clear_start and hue_clear_stop # (marked with 1 in the ASCII art) the pixel's alpha value will be set to 255 # + If the hue of the pixel is between hue_clear_start_ramp and hue_clear_start (2) # or if it is between hue_clear_stop and hue_clear_stop_ramp (3) the pixel's # alpha value gets proportional adjusted depending the distance to # hue_clear_start / hue_clear_stop # # # ___________________________________ <-- fully transparent # / 11111111111111111111111111111111111` # / 221111111111111111111111111111111111133` # / 2222111111111111111111111111111111111113333` # _______/ 22222211111111111111111111111111111111111333333`___________ <-- fully opaque # # ^ ^ ^ ^ # | | | | # | ` hue_clear_start | ` hue_clear_stop_ramp # | | # ` hue_clear_start_ramp ` hue_clear_stop # def clear_hue_range_with_linear_ramp! hue_clear_start_ramp, hue_clear_start, hue_clear_stop, hue_clear_stop_ramp, min_luminance start_ramp_size = smallest_delta_degree(hue_clear_start, hue_clear_start_ramp) stop_ramp_size = smallest_delta_degree(hue_clear_stop_ramp, hue_clear_stop) @pixels.each_with_index do |color, index| hue, saturation, luminance = ChunkyPNG::Color::rgb_to_hsl(color) # skip pixel if it is nearly black, not really necesssary, since these colors tend to be perceived as black anyways next if luminance < min_luminance if in_degree_range(hue_clear_start, hue_clear_stop, hue) # (1) @pixels[index] = ajust_alpha(color, 255) elsif in_degree_range(hue_clear_start_ramp, hue_clear_start, hue) # (2) alpha = (smallest_delta_degree(hue_clear_start, hue) / start_ramp_size.to_f * 255.0).to_i @pixels[index] = ajust_alpha(color, alpha) elsif in_degree_range(hue_clear_stop, hue_clear_stop_ramp, hue) # (3) alpha = (smallest_delta_degree(hue_clear_stop, hue) / stop_ramp_size.to_f * 255.0).to_i @pixels[index] = ajust_alpha(color, alpha) end end end end input = ChunkyPNG::Image.from_file('input.png') grayscale = input.dup # work on copy of input grayscale.clear_hue_range_with_linear_ramp!(176, 189, 214, 230, 12) # remove the blue crayon from grayscale image # grayscale.save("debug.png") grayscale.grayscale! # transform image into grayscale input.compose(grayscale).save("output.png") # copy over grayscale image on top of input image
-
Finished in 16th place with a final score of 2.6/5. (View the Gist)README.md
Code Brawl #10: Selective Color With ChunkyPNG
Code Brawl #10: Selective Color With ChunkyPNG
ColorFiltercan be used to selectively color apng. The following colors are supported:blue,green,purple,red,yellowExample:
# load png png = ColorFilter.new 'input.png' # save png with purple selective coloring png.filter('purple').save 'output.png' # save png for each supported color ['purple','blue','green','yellow','red'].each do |c| png.filter(c).save("output_#{c}.png") end.output_0_purple.png
.output_1_blue.png
.output_2_green.png
.output_4_red.png
.output_3_yellow.png
color_filter.rbView full entryrequire 'chunky_png' class ColorFilter def initialize(png) @png = ChunkyPNG::Image.from_file(png) end def filter(c) valid_c = respond_to?("#{c}?") filter = @png filter.pixels.each_with_index do |v, i| if !valid_c || !method("#{c}?").call(v) filter.pixels[i] = ChunkyPNG::Color.to_grayscale(v) end end filter end def blue?(c) r(c) < 110 && g(c) > 100 && b(c) > 150 end def green?(c) r(c) > 50 && r(c) < 200 && g(c) > 120 && b(c) < 150 end def purple?(c) r(c) > 0 && g(c) < 105 && b(c) > 80 end def red?(c) r(c) > 50 && g(c) < 90 && b(c) < 100 end def yellow?(c) r(c) > 200 && g(c) > 110 && b(c) < 200 end private def r(c); ChunkyPNG::Color.r(c); end def g(c); ChunkyPNG::Color.g(c); end def b(c); ChunkyPNG::Color.b(c); end end