Skip to content

Instantly share code, notes, and snippets.

@peplin
Created September 14, 2011 20:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peplin/1217763 to your computer and use it in GitHub Desktop.
Save peplin/1217763 to your computer and use it in GitHub Desktop.

Selective Desaturation

It's not perfect, but this solution uses Delta-E color differerence formula from the Lab color space (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 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')
require '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')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment