I want to detect the bounding rectangle of an German ID card within an image by using OpenCV.
This is what my code looks like:
capture >> frame;
cv::resize(frame, frame, cv::Size(512,256));
cv::Mat grayScaledFrame, blurredFrame, cannyFrame;
cv::cvtColor(frame, grayScaledFrame, cv::COLOR_BGR2GRAY);
cv::GaussianBlur(grayScaledFrame, blurredFrame, cv::Size(9,9), 1);
cv::Canny(blurredFrame, cannyFrame, 40, 70);
// CONTOURS
std::vector<std::vector<cv::Point>> contours;
cv::findContours(cannyFrame, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// SORT
int maxArea = 0;
std::vector<cv::Point> contour;
for(int i = 0; i < contours.size(); i++) {
int thisArea = cv::contourArea(contours.at(i));
if(thisArea > maxArea) {
maxArea = thisArea;
contour = contours.at(i);
}
}
cv::Rect borderBox = cv::boundingRect(contour);
cv::rectangle(cannyFrame, borderBox, cv::Scalar{255, 32, 32}, 8);
cv::imshow("Webcam", cannyFrame);
The result looks like this:
RESULT
There are some rectangles detected but not the big one I'm interested in.
I've already tried different thresholds for Canny and also different kernel sizes for Gaussian Blur.
Best regards
First of all, as the environmental conditions change, the parameters of the code change, so it is necessary to standardize the environment (light, distance to the object, etc.).
To get this detection right, put the card at a fixed distance from the camera and calculate the area of the rectangles.
When the card is at a certain distance from the camera, you get approximate reference values of the card's area. Then, when drawing a rectangle, you use values within a specified tolerance range.
Related
I'm using opencv 2.4.13
I'm trying to find the perimeter of a connected component, I was thinking of using ConnectedComponentWithStats but it doesn't return the perimeter, only the area, width, etc...
There is a method to find the area with the contour but not the opposite (with one component i mean, not the entire image).
The method arcLength doesn't work as well beause i have all the points of the component, not only the contour.
I know there is a BF way to find it by iterating through each pixel of the component and see if he has neighbors which aren't in the same component. But I'd like a function which costs less.
Otherwise, if you know a way to link a component with the contours found by the method findContours, it suits me as well.
Thanks
Adding to #Miki's answer, This is a faster way to find the perimeter of the connected component
//getting the connected components with statistics
cv::Mat1i labels, stats;
cv::Mat centroids;
int lab = connectedComponentsWithStats(img, labels, stats, centroids);
for (int i = 1; i < lab; ++i)
{
//Rectangle around the connected component
cv::Rect rect(stats(i, 0), stats(i, 1), stats(i, 2), stats(i, 3));
// Get the mask for the i-th contour
Mat1b mask_i = labels(rect) == i;
// Compute the contour
vector<vector<Point>> contours;
findContours(mask_i, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
if(contours.size() <= 0)
continue;
//Finding the perimeter
double perimeter = contours[0].size();
//you can use this as well for measuring perimeter
//double perimeter = arcLength(contours[0], true);
}
The easiest thing is probably to use findContours.
You can compute the contour on the i-th component computed by connectedComponents(WithStats) , so they are aligned with your labels. Using CHAIN_APPROX_NONE you'll get all the points in the contour, so the size() of the vector is already a measure of the perimeter. You can eventually use arcLength(...) to get a more accurate result:
Mat1i labels;
int n_labels = connectedComponents(img, labels);
for (int i = 1; i < n_labels; ++i)
{
// Get the mask for the i-th contour
Mat1b mask_i = labels == i;
// Compute the contour
vector<vector<Point>> contours;
findContours(mask_i.clone(), contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
if (!contours.empty())
{
// The first contour (and probably the only one)
// is the one you're looking for
// Compute the perimeter
double perimeter_i = contours[0].size();
}
}
I'm playing around with OpenCV and I want to know how you would build a simple version of a perspective transform program. I have a image of a parallelogram and each corner of it consists of a pixel with a specific color, which is nowhere else in the image. I want to iterate through all pixels and find these 4 pixels. Then I want to use them as corner points in a new image in order to warp the perspective of the original image. In the end I should have a zoomed on square.
Point2f src[4]; //Is this the right datatype to use here?
int lineNumber=0;
//iterating through the pixels
for(int y = 0; y < image.rows; y++)
{
for(int x = 0; x < image.cols; x++)
{
Vec3b colour = image.at<Vec3b>(Point(x, y));
if(color.val[1]==245 && color.val[2]==111 && color.val[0]==10) {
src[lineNumber]=this pixel // something like Point2f(x,y) I guess
lineNumber++;
}
}
}
/* I also need to get the dst points for getPerspectiveTransform
and afterwards warpPerspective, how do I get those? Take the other
points, check the biggest distance somehow and use it as the maxlength to calculate
the rest? */
How should you use OpenCV in order to solve the problem? (I just guess I'm not doing it the "normal and clever way") Also how do I do the next step, which would be using more than one pixel as a "marker" and calculate the average point in the middle of multiple points. Is there something more efficient than running through each pixel?
Something like this basically:
Starting from an image with colored circles as markers, like:
Note that is a png image, i.e. with a loss-less compression which preserves the actual color. If you use a lossy compression like jpeg the colors will change a little, and you cannot segment them with an exact match, as done here.
You need to find the center of each marker.
Segment the (known) color, using inRange
Find all connected components with the given color, with findContours
Find the largest blob, here done with max_element with a lambda function, and distance. You can use a for loop for this.
Find the center of mass of the largest blob, here done with moments. You can use a loop also here, eventually.
Add the center to your source vertices.
Your destination vertices are just the four corners of the destination image.
You can then use getPerspectiveTransform and warpPerspective to find and apply the warping.
The resulting image is:
Code:
#include <opencv2/opencv.hpp>
#include <vector>
#include <algorithm>
using namespace std;
using namespace cv;
int main()
{
// Load image
Mat3b img = imread("path_to_image");
// Create a black output image
Mat3b out(300,300,Vec3b(0,0,0));
// The color of your markers, in order
vector<Scalar> colors{ Scalar(0, 0, 255), Scalar(0, 255, 0), Scalar(255, 0, 0), Scalar(0, 255, 255) }; // red, green, blue, yellow
vector<Point2f> src_vertices(colors.size());
vector<Point2f> dst_vertices = { Point2f(0, 0), Point2f(0, out.rows - 1), Point2f(out.cols - 1, out.rows - 1), Point2f(out.cols - 1, 0) };
for (int idx_color = 0; idx_color < colors.size(); ++idx_color)
{
// Detect color
Mat1b mask;
inRange(img, colors[idx_color], colors[idx_color], mask);
// Find connected components
vector<vector<Point>> contours;
findContours(mask, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
// Find largest
int idx_largest = distance(contours.begin(), max_element(contours.begin(), contours.end(), [](const vector<Point>& lhs, const vector<Point>& rhs) {
return lhs.size() < rhs.size();
}));
// Find centroid of largest component
Moments m = moments(contours[idx_largest]);
Point2f center(m.m10 / m.m00, m.m01 / m.m00);
// Found marker center, add to source vertices
src_vertices[idx_color] = center;
}
// Find transformation
Mat M = getPerspectiveTransform(src_vertices, dst_vertices);
// Apply transformation
warpPerspective(img, out, M, out.size());
imshow("Image", img);
imshow("Warped", out);
waitKey();
return 0;
}
I'm currently making a program to track 4 paddles, with 3 different colors. I'm having trouble understanding how best to proceed, with the knowledge I have now, and how to reduce the computational costs of running the project. There are code examples of the steps listed at the end of this post.
The program contains a class file called Controllers, that has simple get and set functions for things such as X and Y position, and which HSV values are used for thresholding.
The program in its unoptimized state now does the following:
Reads image from webcam
Converts image to HSV colorspace
Uses the inRange function of OpenCV, together with some previously defined max/min values for HSV, to threshold the HSV image 3 times, one for each colored paddle. This saves to seperate Mat arrays.
(This step is problematic for me) - Performs erosion and dilation of EACH of the three thresholded images.
Passes the image into a function, that uses Moments to create a vector of point describing the contours, and then uses moments to calculate the X and Y location, which is saved as an object and pushed back into a vector of these paddle objects.
Everything technically works at this point, but the resources required to perform the morphological operations three times each loop through the while loop that reads images from the webcam is slowing the program immensely.(Applying 2 iterations of erosion, and 3 of dilation on 3 640*480 images at an acceptable frame rate.)
Threshold Images for different paddles
inRange(HSV, playerOne.getHSVmin(), playerOne.getHSVmax(), threshold1);
inRange(HSV, playerTwo.getHSVmin(), playerTwo.getHSVmax(), threshold2);
inRange(HSV, powerController.getHSVmin(), powerController.getHSVmax(), threshold3);
Perform morphological operations
morphOps(threshold1);
void morphOps(Mat &thresh)
{
//Create a structuring element to be used for morph operations.
Mat structuringElement = getStructuringElement(MORPH_RECT, Size(3,3));
Mat dilateElement = getStructuringElement(MORPH_RECT, Size(6, 6));
//Perform the morphological operations, using two/three iterations because the noise is horrible.
erode(thresh, thresh, structuringElement, Point(-1, -1), 3);
dilate(thresh, thresh, dilateElement, Point(-1, -1), 2);
}
Track the image
trackFilteredObject(playerOne, threshold1, cameraFeed);
trackFilteredObject(playerTwo, threshold2, cameraFeed);
trackFilteredObject(powerController, threshold3, cameraFeed);
void trackFilteredObject(Controllers theControllers, Mat threshold, Mat HSV, Mat &cameraFeed)
{
vector <Controllers> players;
Mat temp;
threshold.copyTo(temp);
//these vectors are needed to save the output of findCountours
vector< vector<Point> > contours;
vector<Vec4i> hierarchy;
//Find the contours of the image
findContours(temp, contours, hierarchy, CV_RETR_CCOMP, CV_CHAIN_APPROX_SIMPLE);
//Moments are used to find the filtered objects.
double refArea = 0;
bool objectFound = false;
if (hierarchy.size() > 0)
{
int numObjects = hierarchy.size();
//If there are more objects than the maximum number of objects we want to track, the filter may be noisy.
if (numObjects < MAX_NUM_OBJECTS)
{
for (int i = 0; i >= 0; i = hierarchy[i][0])
{
Moments moment = moments((Mat)contours[i]);
double area = moment.m00;
//If the area is less than min area, then it is probably noise
if (area > MIN_AREA)
{
Controllers player;
player.setXPos(moment.m10 / area);
player.setYPos(moment.m01 / area);
player.setType(theControllers.getType());
player.setColor(theControllers.getColor());
players.push_back(player);
objectFound = true;
}
else objectFound = false;
}
//Draw the object location on screen if an object is found
if (objectFound)
{
drawObject(players, cameraFeed);
}
}
}
}
The idea is that I want to be able to isolate each object, and use the X and Y positions as points of a triangle, and use the information to calculate angle and power of an arrow shot. So I want to know if there is a better way to isolate the colored paddles and remove the noise, that doesn't require me to perform these morphological operations for each color.
What I'm trying to do is measure the thickness of the eyeglasses frames. I had the idea to measure the thickness of the frame's contours (may be a better way?). I have so far outlined the frame of the glasses, but there are gaps where the lines don't meet. I thought about using HoughLinesP, but I'm not sure if this is what I need.
So far I have conducted the following steps:
Convert image to grayscale
Create ROI around the eye/glasses area
Blur the image
Dilate the image (have done this to remove any thin framed glasses)
Conduct Canny edge detection
Found contours
These are the results:
This is my code so far:
//convert to grayscale
cv::Mat grayscaleImg;
cv::cvtColor( img, grayscaleImg, CV_BGR2GRAY );
//create ROI
cv::Mat eyeAreaROI(grayscaleImg, centreEyesRect);
cv::imshow("roi", eyeAreaROI);
//blur
cv::Mat blurredROI;
cv::blur(eyeAreaROI, blurredROI, Size(3,3));
cv::imshow("blurred", blurredROI);
//dilate thin lines
cv::Mat dilated_dst;
int dilate_elem = 0;
int dilate_size = 1;
int dilate_type = MORPH_RECT;
cv::Mat element = getStructuringElement(dilate_type,
cv::Size(2*dilate_size + 1, 2*dilate_size+1),
cv::Point(dilate_size, dilate_size));
cv::dilate(blurredROI, dilated_dst, element);
cv::imshow("dilate", dilated_dst);
//edge detection
int lowThreshold = 100;
int ratio = 3;
int kernel_size = 3;
cv::Canny(dilated_dst, dilated_dst, lowThreshold, lowThreshold*ratio, kernel_size);
//create matrix of the same type and size as ROI
Mat dst;
dst.create(eyeAreaROI.size(), dilated_dst.type());
dst = Scalar::all(0);
dilated_dst.copyTo(dst, dilated_dst);
cv::imshow("edges", dst);
//join the lines and fill in
vector<Vec4i> hierarchy;
vector<vector<Point>> contours;
cv::findContours(dilated_dst, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE);
cv::imshow("contours", dilated_dst);
I'm not entirely sure what the next steps would be, or as I said above, if I should use HoughLinesP and how to implement it. Any help is very much appreciated!
I think there are 2 main problems.
segment the glasses frame
find the thickness of the segmented frame
I'll now post a way to segment the glasses of your sample image. Maybe this method will work for different images too, but you'll probably have to adjust parameters, or you might be able to use the main ideas.
Main idea is:
First, find the biggest contour in the image, which should be the glasses. Second, find the two biggest contours within the previous found biggest contour, which should be the glasses within the frame!
I use this image as input (which should be your blurred but not dilated image):
// this functions finds the biggest X contours. Probably there are faster ways, but it should work...
std::vector<std::vector<cv::Point>> findBiggestContours(std::vector<std::vector<cv::Point>> contours, int amount)
{
std::vector<std::vector<cv::Point>> sortedContours;
if(amount <= 0) amount = contours.size();
if(amount > contours.size()) amount = contours.size();
for(int chosen = 0; chosen < amount; )
{
double biggestContourArea = 0;
int biggestContourID = -1;
for(unsigned int i=0; i<contours.size() && contours.size(); ++i)
{
double tmpArea = cv::contourArea(contours[i]);
if(tmpArea > biggestContourArea)
{
biggestContourArea = tmpArea;
biggestContourID = i;
}
}
if(biggestContourID >= 0)
{
//std::cout << "found area: " << biggestContourArea << std::endl;
// found biggest contour
// add contour to sorted contours vector:
sortedContours.push_back(contours[biggestContourID]);
chosen++;
// remove biggest contour from original vector:
contours[biggestContourID] = contours.back();
contours.pop_back();
}
else
{
// should never happen except for broken contours with size 0?!?
return sortedContours;
}
}
return sortedContours;
}
int main()
{
cv::Mat input = cv::imread("../Data/glass2.png", CV_LOAD_IMAGE_GRAYSCALE);
cv::Mat inputColors = cv::imread("../Data/glass2.png"); // used for displaying later
cv::imshow("input", input);
//edge detection
int lowThreshold = 100;
int ratio = 3;
int kernel_size = 3;
cv::Mat canny;
cv::Canny(input, canny, lowThreshold, lowThreshold*ratio, kernel_size);
cv::imshow("canny", canny);
// close gaps with "close operator"
cv::Mat mask = canny.clone();
cv::dilate(mask,mask,cv::Mat());
cv::dilate(mask,mask,cv::Mat());
cv::dilate(mask,mask,cv::Mat());
cv::erode(mask,mask,cv::Mat());
cv::erode(mask,mask,cv::Mat());
cv::erode(mask,mask,cv::Mat());
cv::imshow("closed mask",mask);
// extract outermost contour
std::vector<cv::Vec4i> hierarchy;
std::vector<std::vector<cv::Point>> contours;
//cv::findContours(mask, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE);
cv::findContours(mask, contours, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
// find biggest contour which should be the outer contour of the frame
std::vector<std::vector<cv::Point>> biggestContour;
biggestContour = findBiggestContours(contours,1); // find the one biggest contour
if(biggestContour.size() < 1)
{
std::cout << "Error: no outer frame of glasses found" << std::endl;
return 1;
}
// draw contour on an empty image
cv::Mat outerFrame = cv::Mat::zeros(mask.rows, mask.cols, CV_8UC1);
cv::drawContours(outerFrame,biggestContour,0,cv::Scalar(255),-1);
cv::imshow("outer frame border", outerFrame);
// now find the glasses which should be the outer contours within the frame. therefore erode the outer border ;)
cv::Mat glassesMask = outerFrame.clone();
cv::erode(glassesMask,glassesMask, cv::Mat());
cv::imshow("eroded outer",glassesMask);
// after erosion if we dilate, it's an Open-Operator which can be used to clean the image.
cv::Mat cleanedOuter;
cv::dilate(glassesMask,cleanedOuter, cv::Mat());
cv::imshow("cleaned outer",cleanedOuter);
// use the outer frame mask as a mask for copying canny edges. The result should be the inner edges inside the frame only
cv::Mat glassesInner;
canny.copyTo(glassesInner, glassesMask);
// there is small gap in the contour which unfortunately cant be closed with a closing operator...
cv::dilate(glassesInner, glassesInner, cv::Mat());
//cv::erode(glassesInner, glassesInner, cv::Mat());
// this part was cheated... in fact we would like to erode directly after dilation to not modify the thickness but just close small gaps.
cv::imshow("innerCanny", glassesInner);
// extract contours from within the frame
std::vector<cv::Vec4i> hierarchyInner;
std::vector<std::vector<cv::Point>> contoursInner;
//cv::findContours(glassesInner, contoursInner, hierarchyInner, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE);
cv::findContours(glassesInner, contoursInner, hierarchyInner, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
// find the two biggest contours which should be the glasses within the frame
std::vector<std::vector<cv::Point>> biggestInnerContours;
biggestInnerContours = findBiggestContours(contoursInner,2); // find the one biggest contour
if(biggestInnerContours.size() < 1)
{
std::cout << "Error: no inner frames of glasses found" << std::endl;
return 1;
}
// draw the 2 biggest contours which should be the inner glasses
cv::Mat innerGlasses = cv::Mat::zeros(mask.rows, mask.cols, CV_8UC1);
for(unsigned int i=0; i<biggestInnerContours.size(); ++i)
cv::drawContours(innerGlasses,biggestInnerContours,i,cv::Scalar(255),-1);
cv::imshow("inner frame border", innerGlasses);
// since we dilated earlier and didnt erode quite afterwards, we have to erode here... this is a bit of cheating :-(
cv::erode(innerGlasses,innerGlasses,cv::Mat() );
// remove the inner glasses from the frame mask
cv::Mat fullGlassesMask = cleanedOuter - innerGlasses;
cv::imshow("complete glasses mask", fullGlassesMask);
// color code the result to get an impression of segmentation quality
cv::Mat outputColors1 = inputColors.clone();
cv::Mat outputColors2 = inputColors.clone();
for(int y=0; y<fullGlassesMask.rows; ++y)
for(int x=0; x<fullGlassesMask.cols; ++x)
{
if(!fullGlassesMask.at<unsigned char>(y,x))
outputColors1.at<cv::Vec3b>(y,x)[1] = 255;
else
outputColors2.at<cv::Vec3b>(y,x)[1] = 255;
}
cv::imshow("output", outputColors1);
/*
cv::imwrite("../Data/Output/face_colored.png", outputColors1);
cv::imwrite("../Data/Output/glasses_colored.png", outputColors2);
cv::imwrite("../Data/Output/glasses_fullMask.png", fullGlassesMask);
*/
cv::waitKey(-1);
return 0;
}
I get this result for segmentation:
the overlay in original image will give you an impression of quality:
and inverse:
There are some tricky parts in the code and it's not tidied up yet. I hope it's understandable.
The next step would be to compute the thickness of the the segmented frame. My suggestion is to compute the distance transform of the inversed mask. From this you will want to compute a ridge detection or skeletonize the mask to find the ridge. After that use the median value of ridge distances.
Anyways I hope this posting can help you a little, although it's not a solution yet.
Depending on lighting, frame color etc this may or may not work but how about simple color detection to separate the frame ? Frame color will usually be a lot darker than human skin. You'll end up with a binary image (just black and white) and by calculating the number (area) of black pixels you get the area of the frame.
Another possible way is to get better edge detection, by adjusting/dilating/eroding/both until you get better contours. You will also need to differentiate the contour from the lenses and then apply cvContourArea.
I'm trying to locate some regions of a frame, the frame is in Ycbcr color space. and I have to select those regions based on their Y values.
so I wrote this code:
Mat frame. ychannel;
VideoCapture cap(1);
int key =0;
int maxV , minV;
Point max, min;
while(key != 27){
cap >> frame;
cvtColor(frame,yframe,CV_RGB_YCrCb); // converting to YCbCr color space
extractChannel(yframe, yframe, 0); // extracting the Y channel
cv::minMaxLoc(yframe,&minV,&maxV,&min,&max);
cv::threshold(outf,outf,(maxV-10),(maxV),CV_THRESH_TOZERO);
/**
Now I want to use :
cv::rectangle()
but I want to draw a rect around any pixel (see the picture bellow)that's higher than (maxV-10)
and that during the streaming
**/
key = waitKey(1);
}
I draw this picture hopping that it helps to understand what I what to do .
thanks for your help.
Once you have applied your threshold you will end up with a binary image containing a number of connected components, if you want to draw a rectangle around each component then you first need to detect those components.
The OpenCV function findContours does just that, pass it your binary image, and it will provide you with a vector of vectors of points which trace the boundary of each component in your image.
cv::Mat binaryImage;
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binaryImage, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE)
Then all you need to do is find the bounding rectangle of each of those sets of points and draw them to your output image.
for (int i=0; i<contours.size(); ++i)
{
cv::Rect r = cv::boundingRect(contours.at(i));
cv::rectangle(outputImage, r, CV_RGB(255,0,0));
}
You have to find the each of the connected components, and draw their bounding box.