OpenCV library is very popular and widely used in computer vision. OpenCV is very easy to use but one should be familiar with different aspects of image processing and how to effectively deal with them before moving to real-world challenges. In this article, we will go through the process of shape detection using OpenCV with C++. In this exercise, I've used Microsoft Visual Studio 2019 and OpenCV 4.5.
The process of object/shape detection involves below key steps.
Preprocess input image
Edge detection and contour drawing
Classifying objects based on edges
Let's have a closer look at each of the steps and try to implement them in C++ source code. For this exercise, we will use the below image as an input image which contains different kinds of shapes and a few black contours as noise.
Preprocessing Input Image
We need to preprocess the input image before we apply contour identifier and other operations to find no. of edges. Below image 3 shows different stages involved in preprocessing of the input image.
Below is a snippet of code for the steps mentioned in Image 3.
string path = "Resources/color_shapes.jpg";
Mat img = imread(path);
// Declaring few Mat object for further operations
Matimg_gray, img_blur, img_canny, img_dilate;
// Convert img color space. Output image is second arg
cvtColor(img,img_gray,COLOR_BGR2GRAY);
// Blurring image using gaussian fliter. Size(3,3) is SE kernal
GaussianBlur(img_gray, img_blur, Size(3, 3), 3, 0);
// Edge detection using canny algo
Canny(img_blur, img_canny, 25, 75);
// Running dilation on canny output to improve edge thickness
Mat se1 = getStructuringElement(MORPH_RECT, Size(3, 3));
dilate(img_canny, img_dilate, se1);
Below is the output of applying the canny edge detector and dilation on the grey scaled input image.
Edge detection and contour drawing
Once we've preprocessed the grayscale image to obtain an image with edges, we need to apply the below steps as mentioned in image 5.
Below is a snippet of code for the steps mentioned in Image 5.
// dial_img is preprocessed image from earlier step
// in_img is RGB input image
void get_shape_contours(Mat& dial_img, Mat& in_img)
{
// vector of points to store contour points
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(dial_img, contours, hierarchy, RETR_EXTERNAL,
CHAIN_APPROX_SIMPLE);
vector<vector<Point>> contourPoly(contours.size());
// bouning box/rect around shape
vector<Rect> bound_rect(contours.size());
// loop through all contours detected
for (int i = 0; i < contours.size();++i)
{
int c_area = contourArea(contours[i]); // area of each object contour
if (c_area > 1000) // Area based threshold for emoving noise
{
float peri = arcLength(contours[i], true);
// Approximate poly curve with stated accurracy
approxPolyDP(contours[i], contourPoly[i], 0.02 * peri, true);
// Finds no. of points in countours and draw lines(purple)
drawContours(in_img, contourPoly, i, Scalar(255, 0, 255), 2);
bound_rect[i] = boundingRect(contourPoly[i]);
// Draw bounding rectangle around detected objects of input img
rectangle(in_img,
bound_rect[i].tl(), //bounding rectanle top left point
bound_rect[i].br(), //bounding rectanle bottom right point
Scalar(0, 255, 0), 3); // left/ bottom right points
}
} // end of for loop
}
Classifying objects based on edges
In the earlier stage using function get_shape_contours(), we've looped through the different detected objects and removed the noise-based of object's contour size. In order to identify shape type, we need to simply extend the earlier function get_shape_contours() by calculating no. of edges/corners the detected object has. If a detected object has 3 edges then it means its shape is a triangle. Similarly, for other shape types, we can write if-else or switch statements. Below is the code snippet to classify shape types based on no. of edges and annotate the input image with shape type.
// annotating image with shape type
int obj_corners = (int)contourPoly[i].size();
string obj_name;
if (obj_corners == 3)
obj_name = "Triangle";
else if (obj_corners == 4) // rectangle or square
{
float aspect_ratio = (float)bound_rect[i].width /
(float) bound_rect[i].height;
// tolerance for l/w ratio
if(aspect_ratio>0.95 && aspect_ratio<1.05)
obj_name = "Square";
else
obj_name = "Rectangle";
}
else if (obj_corners > 6)
obj_name = "Circle";
// Adding shape type on input image
// y axis offset to ensure text is above detected shape
Point text_point { bound_rect[i].x, bound_rect[i].y - 5 };
putText(in_img, obj_name, // shape type
text_point , // x,y co-ordinate
FONT_HERSHEY_PLAIN, // Font name
1, // Font scale
Scalar(0, 0, 255), // Font color in BGR (Red)
1); // Thickness
Below is the zip folder containing the source code along with the image for further reference. Once we combine different parts of the above-mentioned steps and execute the application it gives the below output (Image 6).
int main()
{
string path = "Resources/color_shapes.jpg";
Mat img = imread(path);
Mat img_gray, img_blur, img_canny, img_dilate, img_erode;
//--------------------------------------------------------------------
// convert img color space. Output image is second arg
cvtColor(img,img_gray,COLOR_BGR2GRAY);
// blurring image using gaussian fliter
GaussianBlur(img_gray, img_blur, Size(3, 3), 3, 0);
Canny(img_blur, img_canny, 25, 75); // edge detection using canny algo
// Running dilation on canny output to improve edge thickness
Mat se1 = getStructuringElement(MORPH_RECT, Size(3, 3));
dilate(img_canny, img_dilate, se1);
//--------------------------------------------------------------------
// draw contour and print shape details on input image
get_shape_contours(img_dilate, img);
imshow("BGR image", img);
waitKey(0);
return 0;
}
Comments