Image processing: Contours – Creating bounding boxes and circles for contours OpenCV v4.8.0

Previous tutorial: Convex hull

Next tutorial: Creating rotated bounding boxes and ellipses for contours

Original author Ana Huamán
Compatibility OpenCV >= 3.0

Goals

In this tutorial you will learn how to

  • Use the OpenCV function cv::boundingRect
  • Use the OpenCV function cv::minEnclosingCircle

Code

C++
The tutorial code is shown below. You can also download it from here

#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>
using namespace cv;
using namespace std;
Mat src_gray;
int thresh = 100;
RNG rng(12345);
void thresh_callback(int, void* );
int main(int argc, char** argv)
{<!-- -->
 CommandLineParser parser( argc, argv, "{@input | stuff.jpg | input image}" );
 Mat src = imread( samples::findFile( parser.get<String>( "@input" ) ) );
 if( src.empty() )
 {<!-- -->
 cout << "Could not open or find the image!\\
" << endl;
 cout << "usage: " << argv[0] << " <Input image>" << endl;
 return -1;
 }
 cvtColor( src, src_gray, COLOR_BGR2GRAY );
 blur( src_gray, src_gray, Size(3,3) );
 const char* source_window = "Source";
 namedWindow( source_window );
 imshow( source_window, src );
 const int max_thresh = 255;
 createTrackbar( "Canny thresh:", source_window, & amp;thresh, max_thresh, thresh_callback );
 thresh_callback(0, 0);
 waitKey();
 return 0;
}
void thresh_callback(int, void* )
{<!-- -->
 Mat canny_output;
 Canny(src_gray, canny_output, thresh, thresh*2);
 vector<vector<Point> > contours;
 findContours( canny_output, contours, RETR_TREE, CHAIN_APPROX_SIMPLE );
 vector<vector<Point> > contours_poly( contours.size() );
 vector<Rect> boundRect( contours.size() );
 vector<Point2f>centers( contours.size() );
 vector<float>radius( contours.size() );
 for( size_t i = 0; i < contours.size(); i + + )
 {<!-- -->
 approxPolyDP( contours[i], contours_poly[i], 3, true );
 boundRect[i] = boundingRect( contours_poly[i] );
 minEnclosingCircle( contours_poly[i], centers[i], radius[i] );
 }
 Mat drawing = Mat::zeros( canny_output.size(), CV_8UC3 );
 for( size_t i = 0; i< contours.size(); i + + )
 {<!-- -->
 Scalar color = Scalar( rng.uniform(0, 256), rng.uniform(0,256), rng.uniform(0,256) );
 drawContours( drawing, contours_poly, (int)i, color );
 rectangle( drawing, boundRect[i].tl(), boundRect[i].br(), color, 2 );
 circle(drawing, centers[i], (int)radius[i], color, 2);
 }
 imshow( "Contours", drawing );
}

Java
The tutorial code is shown below. You can also download it from here

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Image;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
class GeneralContours1 {<!-- -->
 private Mat srcGray = new Mat();
 private JFrame frame;
 private JLabel imgSrcLabel;
 private JLabel imgContoursLabel;
 private static final int MAX_THRESHOLD = 255;
 private int threshold = 100;
 private Random rng = new Random(12345);
 public GeneralContours1(String[] args) {<!-- -->
 String filename = args.length > 0 ? args[0] : "../data/stuff.jpg";
 Mat src = Imgcodecs.imread(filename);
 if (src.empty()) {<!-- -->
 System.err.println("Cannot read image: " + filename);
 System.exit(0);
 }
 Imgproc.cvtColor(src, srcGray, Imgproc.COLOR_BGR2GRAY);
 Imgproc.blur(srcGray, srcGray, new Size(3, 3));
 // Create and set up the window.
 frame = new JFrame("Creating Bounding boxes and circles for contours demo");
 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 // Set up the content pane.
 Image img = HighGui.toBufferedImage(src);
 addComponentsToPane(frame.getContentPane(), img);
 // Use the content pane's default border layout. No need
 // setLayout(new BorderLayout());
 //Show the window.
 frame.pack();
 frame.setVisible(true);
 update();
 }
 private void addComponentsToPane(Container pane, Image img) {<!-- -->
 if (!(pane.getLayout() instanceof BorderLayout)) {<!-- -->
 pane.add(new JLabel("Container doesn't use BorderLayout!"));
 return;
 }
 JPanel sliderPanel = new JPanel();
 sliderPanel.setLayout(new BoxLayout(sliderPanel, BoxLayout.PAGE_AXIS));
 sliderPanel.add(new JLabel("Canny threshold: "));
 JSlider slider = new JSlider(0, MAX_THRESHOLD, threshold);
 slider.setMajorTickSpacing(20);
 slider.setMinorTickSpacing(10);
 slider.setPaintTicks(true);
 slider.setPaintLabels(true);
 slider.addChangeListener(new ChangeListener() {<!-- -->
 @Override
 public void stateChanged(ChangeEvent e) {<!-- -->
 JSlider source = (JSlider) e.getSource();
 threshold = source.getValue();
 update();
 }
 });
 sliderPanel.add(slider);
 pane.add(sliderPanel, BorderLayout.PAGE_START);
 JPanel imgPanel = new JPanel();
 imgSrcLabel = new JLabel(new ImageIcon(img));
 imgPanel.add(imgSrcLabel);
 Mat blackImg = Mat.zeros(srcGray.size(), CvType.CV_8U);
 imgContoursLabel = new JLabel(new ImageIcon(HighGui.toBufferedImage(blackImg)));
 imgPanel.add(imgContoursLabel);
 pane.add(imgPanel, BorderLayout.CENTER);
 }
 private void update() {<!-- -->
 Mat cannyOutput = new Mat();
 Imgproc.Canny(srcGray, cannyOutput, threshold, threshold * 2);
 List<MatOfPoint> contours = new ArrayList<>();
 Mat hierarchy = new Mat();
 Imgproc.findContours(cannyOutput, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE);
 MatOfPoint2f[] contoursPoly = new MatOfPoint2f[contours.size()];
 Rect[] boundRect = new Rect[contours.size()];
 Point[] centers = new Point[contours.size()];
 float[][] radius = new float[contours.size()][1];
 for (int i = 0; i < contours.size(); i + + ) {<!-- -->
 contoursPoly[i] = new MatOfPoint2f();
 Imgproc.approxPolyDP(new MatOfPoint2f(contours.get(i).toArray()), contoursPoly[i], 3, true);
 boundRect[i] = Imgproc.boundingRect(new MatOfPoint(contoursPoly[i].toArray()));
 centers[i] = new Point();
 Imgproc.minEnclosingCircle(contoursPoly[i], centers[i], radius[i]);
 }
 Mat drawing = Mat.zeros(cannyOutput.size(), CvType.CV_8UC3);
 List<MatOfPoint> contoursPolyList = new ArrayList<>(contoursPoly.length);
 for (MatOfPoint2f poly : contoursPoly) {<!-- -->
 contoursPolyList.add(new MatOfPoint(poly.toArray()));
 }
 for (int i = 0; i < contours.size(); i + + ) {<!-- -->
 Scalar color = new Scalar(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256));
 Imgproc.drawContours(drawing, contoursPolyList, i, color);
 Imgproc.rectangle(drawing, boundRect[i].tl(), boundRect[i].br(), color, 2);
 Imgproc.circle(drawing, centers[i], (int) radius[i][0], color, 2);
 }
 imgContoursLabel.setIcon(new ImageIcon(HighGui.toBufferedImage(drawing)));
 frame.repaint();
 }
}
public class GeneralContoursDemo1 {<!-- -->
 public static void main(String[] args) {<!-- -->
 //Load the local OpenCV library
 System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
 // Arrange tasks for the event dispatch thread:
 // Create and display the graphical user interface for this application.
 javax.swing.SwingUtilities.invokeLater(new Runnable() {<!-- -->
 @Override
 public void run() {<!-- -->
 new GeneralContours1(args);
 }
 });
 }
}

Python
The tutorial code is shown below. You can also download it from here

from __future__ import print_function
import cv2 as cv
import numpy as np
import argparse
import random as rng
rng.seed(12345)
def thresh_callback(val):
 threshold = val
 
 canny_output = cv.Canny(src_gray, threshold, threshold * 2)
 
 
 contours, _ = cv.findContours(canny_output, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
 
 
 contours_poly = [None]*len(contours)
 boundRect = [None]*len(contours)
 centers = [None]*len(contours)
 radius = [None]*len(contours)
 for i, c in enumerate(contours):
 contours_poly[i] = cv.approxPolyDP(c, 3, True)
 boundRect[i] = cv.boundingRect(contours_poly[i])
 centers[i], radius[i] = cv.minEnclosingCircle(contours_poly[i])
 
 
 drawing = np.zeros((canny_output.shape[0], canny_output.shape[1], 3), dtype=np.uint8)
 
 
 for i in range(len(contours)):
 color = (rng.randint(0,256), rng.randint(0,256), rng.randint(0,256))
 cv.drawContours(drawing, contours_poly, i, color)
 cv.rectangle(drawing, (int(boundRect[i][0]), int(boundRect[i][1])), \
 (int(boundRect[i][0] + boundRect[i][2]), int(boundRect[i][1] + boundRect[i][3])), color, 2)
 cv.circle(drawing, (int(centers[i][0]), int(centers[i][1])), int(radius[i]), color, 2)
 
 
 cv.imshow('Contours', drawing)
 
parser = argparse.ArgumentParser(description='Code for Creating Bounding boxes and circles for contours tutorial.')
parser.add_argument('--input', help='Path to input image.', default='stuff.jpg')
args = parser.parse_args()
src = cv.imread(cv.samples.findFile(args.input))
if src is None:
 print('Could not open or find the image:', args.input)
 exit(0)
# Convert image to gray and blur
src_gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
src_gray = cv.blur(src_gray, (3,3))
source_window = 'Source'
cv.namedWindow(source_window)
cv.imshow(source_window, src)
max_thresh = 255
thresh = 100 # initial threshold
cv.createTrackbar('Canny thresh:', source_window, thresh, max_thresh, thresh_callback)
thresh_callback(thresh)
cv.waitKey()

Description

The main function is very simple. As you can see from the comments, we do the following:

  • Open the image, convert it to grayscale, and blur it to remove noise.

C++

 CommandLineParser parser( argc, argv, "{@input | stuff.jpg | input image}" );
 Mat src = imread( samples::findFile( parser.get<String>( "@input" ) ) );
 if( src.empty() )
 {<!-- -->
 cout << "Could not open or find the image!\\
" << endl;
 cout << "usage: " << argv[0] << " <Input image>" << endl;
 return -1;
 }
 cvtColor( src, src_gray, COLOR_BGR2GRAY );
 blur( src_gray, src_gray, Size(3,3) );

Java

 String filename = args.length > 0 ? args[0] : "../data/stuff.jpg";
 Mat src = Imgcodecs.imread(filename);
 if (src.empty()) {<!-- -->
 System.err.println("Cannot read image: " + filename);
 System.exit(0);
 }
 Imgproc.cvtColor(src, srcGray, Imgproc.COLOR_BGR2GRAY);
 Imgproc.blur(srcGray, srcGray, new Size(3, 3));

Python

# Load source image
parser = argparse.ArgumentParser(description='Code for Creating Bounding boxes and circles for contours tutorial.')
parser.add_argument('--input', help='Path to input image.', default='stuff.jpg')
args = parser.parse_args()
src = cv.imread(cv.samples.findFile(args.input))
if src is None:
 print('Could not open or find the image:', args.input)
 exit(0)
# Convert image to gray and blur it
src_gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
src_gray = cv.blur(src_gray, (3,3))
  • Create a window titled “Source” and display the source file in it.

C++

 const char* source_window = "Source";
 namedWindow( source_window );
 imshow( source_window, src );

Java

 // Create and set up the window.
 frame = new JFrame("Creating Bounding boxes and circles for contours demo");
 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 // Set up the content pane.
 Image img = HighGui.toBufferedImage(src);
 addComponentsToPane(frame.getContentPane(), img);

Python

# Create Window
source_window = 'Source'
cv.namedWindow(source_window)
cv.imshow(source_window, src)
  • Create a tracking bar on source_window and assign it a callback function. Generally speaking, callback functions are used to react to some kind of signal, in our case the state change of the trackbar. To display both the “Outline” window and the “Source” window, thresh_callback must be called explicitly once.

C++

 const int max_thresh = 255;
 createTrackbar( "Canny thresh:", source_window, & amp;thresh, max_thresh, thresh_callback );
 thresh_callback(0, 0);

Java

 sliderPanel.add(new JLabel("Canny threshold: "));
 JSlider slider = new JSlider(0, MAX_THRESHOLD, threshold);
 slider.setMajorTickSpacing(20);
 slider.setMinorTickSpacing(10);
 slider.setPaintTicks(true);
 slider.setPaintLabels(true);
 slider.addChangeListener(new ChangeListener() {<!-- -->
 @Override
 public void stateChanged(ChangeEvent e) {<!-- -->
 JSlider source = (JSlider) e.getSource();
 threshold = source.getValue();
 update();
 }
 });

Python

max_thresh = 255
thresh = 100 # initial threshold
cv.createTrackbar('Canny thresh:', source_window, thresh, max_thresh, thresh_callback)
thresh_callback(thresh)

The callback function does all the fun work.

  • Use cv::Canny to detect edges in images.

C++

 Mat canny_output;
 Canny(src_gray, canny_output, thresh, thresh*2);

Java

 Mat cannyOutput = new Mat();
 Imgproc.Canny(srcGray, cannyOutput, threshold, threshold * 2);

Python

 # Detect edges using Canny
 canny_output = cv.Canny(src_gray, threshold, threshold * 2)
  • Find contours and save them into vectors contour and hierarchy.

C++

 vector<vector<Point> > contours;
 findContours( canny_output, contours, RETR_TREE, CHAIN_APPROX_SIMPLE );

Java

 List<MatOfPoint> contours = new ArrayList<>();
 Mat hierarchy = new Mat();
 Imgproc.findContours(cannyOutput, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE);

Python

 # Find contours
 contours, _ = cv.findContours(canny_output, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
  • For each contour found, we now perform an approximation of the polygon with an accuracy of ±3, stating that the curve must be closed. We then find a bounding rectangle for each polygon and save it into a boundRect. Finally, we find a minimum enclosing circle for each polygon and save it as center and radius vectors.

C++

 vector<vector<Point> > contours_poly( contours.size() );
 vector<Rect> boundRect( contours.size() );
 vector<Point2f>centers( contours.size() );
 vector<float>radius( contours.size() );
 for( size_t i = 0; i < contours.size(); i + + )
 {<!-- -->
 approxPolyDP( contours[i], contours_poly[i], 3, true );
 boundRect[i] = boundingRect( contours_poly[i] );
 minEnclosingCircle( contours_poly[i], centers[i], radius[i] );
 }

Java

 MatOfPoint2f[] contoursPoly = new MatOfPoint2f[contours.size()];
 Rect[] boundRect = new Rect[contours.size()];
 Point[] centers = new Point[contours.size()];
 float[][] radius = new float[contours.size()][1];
 for (int i = 0; i < contours.size(); i + + ) {<!-- -->
 contoursPoly[i] = new MatOfPoint2f();
 Imgproc.approxPolyDP(new MatOfPoint2f(contours.get(i).toArray()), contoursPoly[i], 3, true);
 boundRect[i] = Imgproc.boundingRect(new MatOfPoint(contoursPoly[i].toArray()));
 centers[i] = new Point();
 Imgproc.minEnclosingCircle(contoursPoly[i], centers[i], radius[i]);
 }

Python

 # Approximate contours to polygons + get bounding rects and circles
 contours_poly = [None]*len(contours)
 boundRect = [None]*len(contours)
 centers = [None]*len(contours)
 radius = [None]*len(contours)
 for i, c in enumerate(contours):
 contours_poly[i] = cv.approxPolyDP(c, 3, True)
 boundRect[i] = cv.boundingRect(contours_poly[i])
 centers[i], radius[i] = cv.minEnclosingCircle(contours_poly[i])

We found everything we needed, now all we have to do is draw.

  • Creates a new unsigned 8-bit character Mat, padded with zeros. It will contain all the shapes we want to draw (rectangles and circles).

C++

 Mat drawing = Mat::zeros( canny_output.size(), CV_8UC3 );

Java

 Mat drawing = Mat.zeros(cannyOutput.size(), CvType.CV_8UC3);

Python

 drawing = np.zeros((canny_output.shape[0], canny_output.shape[1], 3), dtype=np.uint8)
  • For each contour: randomly select a color and use it to draw the contour, bounding rectangle and minimum enclosing circle.

C++

 for( size_t i = 0; i< contours.size(); i + + )
 {<!-- -->
 Scalar color = Scalar( rng.uniform(0, 256), rng.uniform(0,256), rng.uniform(0,256) );
 drawContours( drawing, contours_poly, (int)i, color );
 rectangle( drawing, boundRect[i].tl(), boundRect[i].br(), color, 2 );
 circle(drawing, centers[i], (int)radius[i], color, 2);
 }

Java

 List<MatOfPoint> contoursPolyList = new ArrayList<>(contoursPoly.length);
 for (MatOfPoint2f poly : contoursPoly) {<!-- -->
 contoursPolyList.add(new MatOfPoint(poly.toArray()));
 }
 for (int i = 0; i < contours.size(); i + + ) {<!-- -->
 Scalar color = new Scalar(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256));
 Imgproc.drawContours(drawing, contoursPolyList, i, color);
 Imgproc.rectangle(drawing, boundRect[i].tl(), boundRect[i].br(), color, 2);
 Imgproc.circle(drawing, centers[i], (int) radius[i][0], color, 2);
 }

Python

 # Draw polygonal contour + bonding rects + circles
 for i in range(len(contours)):
 color = (rng.randint(0,256), rng.randint(0,256), rng.randint(0,256))
 cv.drawContours(drawing, contours_poly, i, color)
 cv.rectangle(drawing, (int(boundRect[i][0]), int(boundRect[i][1])), \
 (int(boundRect[i][0] + boundRect[i][2]), int(boundRect[i][1] + boundRect[i][3])), color, 2)
 cv.circle(drawing, (int(centers[i][0]), int(centers[i][1])), int(radius[i]), color, 2)
  • Show results: Create a new window “Contour” and display everything we added to the drawing.

C++

 imshow( "Contours", drawing );

Java

 imgContoursLabel.setIcon(new ImageIcon(HighGui.toBufferedImage(drawing)));
 frame.repaint();

Python

 # Show in a window
 cv.imshow('Contours', drawing)

Show results

That’s it: