efficiently threshold red using HSV in OpenCV - c++

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?

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.

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);
}

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).

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

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

Related

Access pixel value of mask using opencv

I got a problem where I need to access pixels of a opencv Mat image container.
I use opencv inRange function to create a mask. In that mask I need to check the value of different pixels, but I won't receive the values I expect to receive.
// convert image to hsv for better color-detection
cv::Mat img_hsv, maskR, maskY, mask1, mask2;
cv::cvtColor(image, img_hsv, cv::COLOR_BGR2HSV);
// Gen lower mask (0-5) and upper mask (175-180) of RED
cv::inRange(img_hsv, cv::Scalar(0, 50, 20), cv::Scalar(5, 255, 255), mask1);
cv::inRange(img_hsv, cv::Scalar(175, 50, 20), cv::Scalar(180, 255, 255), mask2);
// Merge the masks
cv::bitwise_or(mask1, mask2, maskR);
after that I try to read the pixel values where I got extremely high values and even nans, but most of them zeros, which is expected as the mask is only black and white
if (maskR.at<double>(position.x, position.y) == 255)
is there something I'm missing? I tried with double, uchar, int and float
when I print the mask, I can clearly see the 0 and 255 entries(no nans or strange numbers), but when I access them with the at() function, I wont get the same results.
The coordinates of the pixels should be in the range of the Mat as the dimension of the mask is 1080x1920 and non of the coordinates reach over that.
I got the dimension by using cv::size
I finally found the answer to my own question.
It works when I use uchar:
maskR.at<uchar>(position.x, position.y) == 255
I thought this wouldn't work because printing this with std::cout wouldn't give me an output, but the reason for that is that I forgot to cast uchar so it could be printed in the console

Pixels at arrow tip missing when using antialiasing

I am trying to draw an arrow with OpenCV 3.2:
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
using namespace cv;
int main()
{
Mat image(480, 640, CV_8UC3, Scalar(255, 255, 255)); //White background
Point from(320, 240); //Middle
Point to(639, 240); //Right border
arrowedLine(image, from, to, Vec3b(0, 0, 0), 1, LINE_AA, 0, 0.1);
imshow("Arrow", image);
waitKey(0);
return 0;
}
An arrow is drawn, but at the tip some pixels are missing:
To be more precise, two columns of pixels are not colored correctly (zoomed):
If I disable antialiasing, i.e., if I use
arrowedLine(image, from, to, Vec3b(0, 0, 0), 1, LINE_8, 0, 0.1);
instead (note the LINE_8 instead of LINE_AA), the pixels are there, albeit without antialiasing:
I am aware that antialiasing might rely on neighboring pixels, but it seems strange that pixels are not drawn at all at the borders instead of being drawn without antialiasing. Is there a workaround for this issue?
Increasing the X coordinate, e.g. to 640 or 641) makes the problem worse, i.e., more of the arrow head pixels disappear, while the tip still lacks nearly two complete pixel columns.
Extending and cropping the image would solve the neighboring pixels issue, but in my original use case, where the problem appeared, I cannot enlarge my image, i.e., its size must remain constant.
After a quick review, I've found that OpenCV draws AA lines using a Gaussian filter, which contracts the final image.
As I've suggested in comments, you can implement your own function for the AA mode (you can call the original one if AA is disabled) extending the points manually (see code below to have an idea).
Other option may be to increase the line width when using AA.
You may also simulate the AA effect of OpenCV but on the final image (may be slower but helpful if you have many arrows). I'm not an OpenCV expert so I'll write a general scheme:
// Filter radius, the higher the stronger
const int kRadius = 3;
// Image is extended to fit pixels that are not going to be blurred
Mat blurred(480 + kRadius * 2, 640 + kRadius * 2, CV_8UC3, Scalar(255, 255, 255));
// Points moved a according to filter radius (need testing, but the idea is that)
Point from(320, 240 + kRadius);
Point to(639 + kRadius * 2, 240 + kRadius);
// Extended non-AA arrow
arrowedLine(blurred, ..., LINE_8, ...);
// Simulate AA
GaussianBlur(blurred, blurred, Size(kRadius, kRadius), ...);
// Crop image (be careful, it doesn't copy data)
Mat image = blurred(Rect(kRadius, kRadius, 640, 480));
Another option may be to draw the arrow in an image twice as large and the scale it down with a good smoothing filter.
Obviously, last two options will work only if you don't have any previous data on the image. If so, then use a transparent image for temporal drawing and overlay it at the end.

Generate random Pastel colour

I am trying to generate a random pastel colour.
Is it correct to say that a pastel colour has a low value/intensity value (HSV)? Therefore something like this should generate random pastel colours: Vec3b randPastel = Vec3b(rng.uniform(0, 180), rng.uniform(0, 255), rng.uniform(0, 50))
My current function fails. It only ever creates black BGR colours for some reason:
Vec3b randPastelBGR()
{
Mat hsv(1, 1, CV_8UC3);
cvtColor(hsv, hsv, CV_BGR2HSV);
hsv.at<Vec3b>(0, 0) = Vec3b(rng.uniform(0, 180), rng.uniform(0, 255), rng.uniform(0, 50));
cvtColor(hsv, hsv, CV_HSV2BGR);
return hsv.at<Vec3b>(0, 0);
}
Pastels are mostly white, i.e. they have low Saturation. Not zero, though, because that would be entirely white (or grey). As you noticed, with a low Value you get dark colors. You want a fairly high Value, and might not even want a random one. (Exactly which color parts have to be random, and why?)
I needed something similar. I just very quickly tried 105*Rand() + 150 for each value of RGB, and it seemed to work pretty well. That will give you values between 150 and 255, i.e. lighter colors. This wasn't in c++, but the software I'm using makes values of 0 to 1 for Rand().

Opencv create new image using cv::Mat

I'm new to opencv and i'm trying on some sample codes.
in one code, Mat gr(row1,col1,CV_8UC1,scalar(0));
int x = gr.at<uchar> (row,col);
And in another one,
Mat grHistrogram(301,260,CV_8UC1,Scalar(0,0,0));
line(grHistrogram,pt1,pt2,Scalar(255,255,255),1,8,0);
Now my question is if i used scalar(0) instead of scalar(0,0,0) in second code, The code doesn't work.
1.Why this happening since, Both create a Mat image structure.
2.what is the purpose of const cv:Scalar &_s.
I search the Documentaion from Opencv site (opencv.pdf,opencv2refman.pdf) and Oreilly's Opencv book. But couldn't find a explained answer.
I think i'm using the Mat(int _rows,int _cols,int _type,const cv:Scalar &_s) struct.
First, you need the following information to create the image:
Width: 301 pixels
Height: 260 pixels
Each pixel value (intensity) is 0 ~ 255: an 8-bit unsigned integer
Supports all RGB colors: 3 channels
Initial color: black = (B, G, R) = (0, 0, 0)
You can create the Image using cv::Mat:
Mat grHistogram(260, 301, CV_8UC3, Scalar(0, 0, 0));
The 8U means the 8-bit Usigned integer, C3 means 3 Channels for RGB color, and Scalar(0, 0, 0) is the initial value for each pixel. Similarly,
line(grHistrogram,pt1,pt2,Scalar(255,255,255),1,8,0);
is to draw a line on grHistogram from point pt1 to point pt2. The color of line is white (255, 255, 255) with 1-pixel thickness, 8-connected line, and 0-shift.
Sometimes you don't need a RGB-color image, but a simple grayscale image. That is, use one channel instead of three. The type can be changed to CV_8UC1 and you only need to specify the intensity for one channel, Scalar(0) for example.
Back to your problem,
Why this happening since, both create a Mat image structure?
Because you need to specify the type of the Mat. Is it a color image CV_8UC3 or a grayscale image CV_8UC1? They are different. Your program may not work as you think if you use Scalar(255) on a CV_8UC3 image.
What is the purpose of const cv:Scalar &_s ?
cv::Scalar is use to specify the intensity value for each pixel. For example, Scalar(255, 0, 0) is blue and Scalar(0, 0, 0) is black if type is CV_8UC3. Or Scalar(0) is black if it's a CV_8UC1 grayscale image. Avoid mixing them together.
You can create single channel image or multi channel image.
creating single channel image : Mat img(500, 1000, CV_8UC1, Scalar(70));
creating multi channel image : Mat img1(500, 1000, CV_8UC3, Scalar(10, 100, 150));
you can see more example and detail from following page.
https://progtpoint.blogspot.com/2017/01/tutorial-3-create-image.html

OpenCV Remove smaller contours

I want to identify and extract the contour of the largest leaf of the following image using OpenCV and C++.
I applied Canny edge detector to the image and got the following result.
Canny(img_src, img_edge_detected, 20, 60, 3);
Now I want to extract the largest contour (largest leaf) form the image and draw the contour line, but the problem here is the edge line of the largest leaf is not continuous. So I looked in to dialate and morphological close but using those functions I couldn't get a good result to extract the area. Is there any way to get the largest contour in such image?
Note that here I cannot use template matching or any masking kind of things because my final intention is to built a system where a user can upload an image and get the species of the plant. So the system doesn't have any prior idea about the shape of the leaf that user is going to upload.
Please tell me how to find and draw the largest contour here if it is possible.
Thanks.
cant you use hsv color threshoding to track only that leaf and then you can straight away use minmaxloc function to get the area of the largest contour.just an idea try doing it like that.it will work.good luck
Same thing i will do in java please convert it into c++, here BGR to convert HSV then after apply the combination of the yellow, green and brown with specified range and simply perfom bitwise or operation. it will be give to you not zero pixles using opencv function Core.findNonZero(Mat src, Mat dst);
Imgproc.cvtColor(mRgba, mHSV, Imgproc.COLOR_BGR2HSV, 4);
//Yellow
Core.inRange(mHSV, new Scalar(25, 80, 80), new Scalar(36, 255, 255), yellow);
//Green
Core.inRange(mHSV, new Scalar(37, 80, 80), new Scalar(70, 255, 255), green);
//Brown
Core.inRange(mHSV, new Scalar(10, 80, 80), new Scalar(30, 200, 200), brown);
// logical OR mask
Core.bitwise_or(yellow, green, green);
Core.bitwise_or(green, brown, mask);
Imgproc.dilate(mask, mask, new Mat());
// Find non zero pixels
pts = Mat.zeros(mask.size(), mask.type());
Core.findNonZero(mask, pts);
return mask;