22
votes

I'm trying to threshold red pixels in a video stream using OpenCV. I have other colors working quite nicely, but red poses a problem because it wraps around the hue axis (ie. HSV(0, 255, 255) and HSV(179, 255, 255) are both red). The technique I'm using now is less than ideal. Basically:

cvInRangeS(src, cvScalar(0, 135, 135), cvScalar(20, 255, 255), dstA);
cvInRangeS(src, cvScalar(159, 135, 135), cvScalar(179, 255, 255), dstB);
cvOr(dstA, dstB, dst);

This is suboptimal because it requires a branch in the code for red (potential bugs), the allocation of two extra images, and two extra operations when compared to the easy case of blue:

cvInRangeS(src, cvScalar(100, 135, 135), cvScalar(140, 255, 255), dst);

The nicer alternative that occurred to me was to "rotate" the image's colors, so that the target hue is at 90 degrees. Eg.

int rotation = 90 - 179; // 179 = red
cvAddS(src, cvScalar(rotation, 0, 0), dst1);
cvInRangeS(dst1, cvScalar(70, 135, 135), cvScalar(110, 255, 255), dst);

This allows me to treat all colors similarly.

However, the cvAddS operation doesn't wrap the hue values back to 180 when they go below 0, so you lose data. I looked at converting the image to CvMat so that I could subtract from it and then use modulus to wrap the negative values back to the top of the range, but CvMat doesn't seem to support modulus. Of course, I could iterate over every pixel, but I'm concerned that that's going to be very slow.


I've read many tutorials and code samples, but they all seem to conveniently only look at ranges that don't wrap around the hue spectrum, or use solutions that are even uglier (eg. re-implementing cvInRangeS by iterating over every pixel and doing manual comparisons against a color table).

So, what's the usual way to solve this? What's the best way? What are the tradeoffs of each? Is iterating over pixels much slower than using built-in CV functions?

5
Swap the red & green channels, threshold green, and swap back?MSalters

5 Answers

1
votes

You won't believe but I had exactly the same issue and I solved it using simple iteration through Hue (not whole HSV) image.

Is iterating over pixels much slower than using built-in CV functions?

I've just tried to understood cv::inRange function but didn't get it at all (it seems that author used some specific iteration).

8
votes

This is kind of late, but this is what I'd try.

Make the conversion: cvCvtColor(imageBgr, imageHsv, CV_RGB2HSV);

Note, RGB vs Bgr are purposefully being crossed.

This way, red color will be treated in a blue channel and will be centered around 170. There would also be a flip in direction, but that is OK as long as you know to expect it.

2
votes

You can calculate Hue channel in range 0..255 with CV_BGR2HSV_FULL. Your original hue difference of 10 will become 14 (10/180*256), i.e. the hue must be in range 128-14..128+14:

public void inColorRange(CvMat imageBgr, CvMat dst, int color, int threshold) {
    cvCvtColor(imageBgr, imageHsv, CV_BGR2HSV_FULL);
    int rotation = 128 - color;
    cvAddS(imageHsv, cvScalar(rotation, 0, 0), imageHsv);
    cvInRangeS(imageHsv, cvScalar(128-threshold, 135, 135), 
         cvScalar(128+threshold, 255, 255), dst);
}
1
votes

There is a really simple way of doing this.

First make two different color ranges

cv::Mat lower_red_hue_range;
cv::Mat upper_red_hue_range;
cv::inRange(hsv_image, cv::Scalar(0, 100, 100), cv::Scalar(10, 255, 255), lower_red_hue_range);
cv::inRange(hsv_image, cv::Scalar(160, 100, 100), cv::Scalar(179, 255, 255), upper_red_hue_range);

Then combine the two masks using addWeighted

cv::Mat red_hue_mask;
cv::addWeighted(lower_red_hue_range, 1.0, upper_red_hue_range, 1.0, 0.0, red_hue_mask);

Now you can just apply the mask to the image

cv::Mat result;
inputImageMat.copyTo(result, red_hue_mask);

I got the idea from a blog post I found

0
votes

cvAddS(...) is equivalent, at element level, to:

 out = static_cast<dest> ( in + shift );

This static_cast is the problem, because is clips/truncates the values.

A solution would be to shift the data from (0-180) to (x, 255), then apply a non-clipping add with overflow:

 out = uchar(in + (255-180) + rotation );

Now you should be able to use a single InRange call, just shift your red interval according to the above formula