On my desktop computer at work I have both speakers and headphones. When I'm wearing the headphones I want the speakers to be muted. Like most computers, when you plug the headphones in the front, it mutes the line-out on the back, but unfortunately my desktop is under the desk and it'd be impractical to reach under and mute it.
Since I switch between wearing them and not wearing them quite frequently, I wanted something that would automatically mute the speakers when I put the headphones on. As a bonus, I'd like my music to pause or unpause automatically. (I share an office, so leaving the music playing all the time won't work.)
Instead of creating a keyboard shortcut to mute or unmute the speakers, I got a little carried away. I have a webcam, so I thought that I could attach a QR code on the top of the headphones. Then, looking at myself in the webcam, I realized that the color of the headphones might be enough.
Perhaps it'd be possible to analyze the webcam image and determine whether or not I was wearing them by counting green pixels.
I tried grabbing a frame from the webcam with mplayer, but it turned out that the image was too dark until the camera had a chance to adjust itself. It's just as easy to read video frames.
# Count number of green pixels.
#
# "image" is a string containing the RGB data
def count_green image
count = 0
image.chars.each_slice(3) do |r,g,b|
count += 1 if g.ord > 64 && g.ord*2 > r.ord*3 && g.ord*2 > r.ord*3
end
end
width = 640
height = 480
dim = "#{width}x#{height}"
video = IO.popen( "avconv -f video4linux -s #{dim} -i /dev/video0 -pix_fmt rgb24 -f rawvideo -vcodec rawvideo -s #{dim} -", 'rb' )
while image = video.read( width * height * 3 )
headphones_present = count_green(image) > 0
if headphones_present
system "mute-speakers"
else
system "unmute-speakers"
end
end
It turns out that it works perfectly. In the example above, 938 pixels match. When I'm not wearing the headphones, less than a dozen match!
To be safe, I've added in some extra logic to reduce the amount of switches, which adds a slight delay. After using it for a few weeks I'll have the values tuned to better values.
Of course, sampling nearly a million pixels every second is not something that ruby best suited for (it takes 137ms/frame on an i7). Luckily, it's easy to replace just that portion with C code using the RubyInline gem.
require 'inline'
class GreenCounter
inline do |builder|
builder.add_compile_flags '-O3 -Wall'
builder.c_singleton File.read('green.c')
end
end
And the C code:
int green( VALUE image_str) {
int i = 0;
char* image = StringValuePtr( image_str );
int size = RSTRING_LEN( image_str );
int count = 0;
unsigned char r, g, b;
for( i = 0; i < size; i += 3 ) {
r = image[i+0];
g = image[i+1];
b = image[i+2];
if( g > 64 && g*2 > r*3 && g*2 > b*3 )
count++;
}
return count;
}
The C version is able to scan an image in 0.4ms, a 340× speedup!
Full code is available on github.