Previous tutorial: Histogram comparison
Next tutorial: Template matching
Original author | Ana Huamán |
---|---|
Compatibility | OpenCV >= 3.0 |
Goals
In this tutorial you will learn
- What is backprojection and its uses
- How to calculate backprojection using OpenCV function cv::calcBackProject
- How to mix different channels of an image using the OpenCV function cv::mixChannels
Theory
What is backprojection?
- Backprojection is a method of recording how well the pixels of a given image match the distribution of pixels in a histogram model.
- In simple terms for back projection you need to compute the histogram model of a feature and then use it to find that feature in the image.
- Application example: If you have a flesh color histogram (such as a hue-saturation histogram), then you can use it to find flesh color areas in the image:
How does it work?
- Let’s take skin as an example:
- Let’s say you get a skin histogram (hue-saturation) based on the image below. The histogram beyond that will be our model histogram (which we know represents a sample of skin tones). You apply some masks to capture only the histogram of skin areas:
T0
T1
- Now, let’s imagine that you get another hand image (test image) like this: (and its respective histogram):
T2
T3
- All we have to do is use our model histogram (which we know represents skin tone) to detect areas of skin in the test image. Specific steps are as follows
- In each pixel of the test image (i.e. p(i,j)), collect data and find the bin position corresponding to that pixel (i.e. (h[i,j],s[i,j])).
- Find the model histogram in the corresponding bin – (h[i,j],s[i,j]) – and read the bin value.
- Store this binary value into a new image (BackProjection). Additionally, you might consider normalizing the model histogram first so you can see the output on the test image.
- Through the above steps, we can get the BackProjection image of the following test image:
- In terms of statistics, the value stored in BackProjection represents the probability that a pixel in the test image belongs to a skin region, based on the model histogram we used. For example, in our test images, brighter areas are more likely to be skin areas (as is actually the case), while dark areas are less likely (note that these “dark” areas belong to surfaces with some shadows, which in turn Coming over will affect the test results).
Code
- What does this program do?
- Load image
- Convert the original image to HSV format and separate out only the hue channels for the histogram (using the OpenCV function cv::mixChannels)
- Let the user enter the number of partitions to use when calculating the histogram.
- Calculate the histogram (and update the histogram when diversity changes) and backprojection of the same image.
- Displays the backprojection and histogram in a window.
C++
- Downloadable code:
- Click here for the basic version (explained in this tutorial).
- For more advanced stuff (defining masks for skin areas using H-S histograms and floodFill), check out the improved demo
- …or you can check out the classic camshiftdemo in the samples.
- Code Overview
#include "opencv2/imgproc.hpp" #include "opencv2/imgcodecs.hpp" #include "opencv2/highgui.hpp" #include <iostream> using namespace cv; using namespace std; Mat hue; int bins = 25; void Hist_and_Backproj(int, void* ); int main( int argc, char* argv[] ) {<!-- --> CommandLineParser parser( argc, argv, "{@input |Back_Projection_Theory0.jpg| input image}" ); samples::addSamplesDataSearchSubDirectory("doc/tutorials/imgproc/histograms/back_projection/images"); 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; } Mat hsv; cvtColor(src, hsv, COLOR_BGR2HSV); hue.create(hsv.size(), hsv.depth()); int ch[] = {<!-- --> 0, 0 }; mixChannels( & amp;hsv, 1, & amp;hue, 1, ch, 1 ); const char* window_image = "Source image"; namedWindow(window_image); createTrackbar("* Hue bins: ", window_image, & amp;bins, 180, Hist_and_Backproj ); Hist_and_Backproj(0, 0); imshow(window_image, src); // Wait until the user exits the program waitKey(); return 0; } void Hist_and_Backproj(int, void* ) {<!-- --> int histSize = MAX(bins, 2); float hue_range[] = {<!-- --> 0, 180 }; const float* ranges[] = {<!-- --> hue_range }; Mat hist; calcHist( & amp;hue, 1, 0, Mat(), hist, 1, & amp;histSize, ranges, true, false ); normalize( hist, hist, 0, 255, NORM_MINMAX, -1, Mat() ); Mat backproj; calcBackProject( & amp;hue, 1, 0, hist, backproj, ranges, 1, true ); imshow( "BackProj", backproj ); int w = 400, h = 400; int bin_w = cvRound( (double) w / histSize ); Mat histImg = Mat::zeros( h, w, CV_8UC3 ); for (int i = 0; i < bins; i + + ) {<!-- --> rectangle( histImg, Point( i*bin_w, h ), Point( (i + 1)*bin_w, h - cvRound( hist.at<float>(i)*h/255.0 ) ), Scalar( 0, 0, 255 ), FILLED ); } imshow( "Histogram", histImg ); }
Java
- Downloadable code:
- Click here for the basic version (explained in this tutorial).
- For more advanced stuff (defining masks for skin areas using H-S histograms and floodFill), check out the improved demo
- …or you can check out the classic camshiftdemo in the samples.
- Code Overview
import java.awt.BorderLayout; import java.awt.Container; import java.awt.Image; import java.util.Arrays; import java.util.List; 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.MatOfFloat; import org.opencv.core.MatOfInt; import org.opencv.core.Point; import org.opencv.core.Scalar; import org.opencv.highgui.HighGui; import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; class CalcBackProject1 {<!-- --> private Mat hue; private Mat histImg = new Mat(); private JFrame frame; private JLabel imgLabel; private JLabel backprojLabel; private JLabel histImgLabel; private static final int MAX_SLIDER = 180; private int bins = 25; public CalcBackProject1(String[] args) {<!-- --> if (args.length != 1) {<!-- --> System.err.println("You must supply one argument that corresponds to the path to the image."); System.exit(0); } Mat src = Imgcodecs.imread(args[0]); if (src.empty()) {<!-- --> System.err.println("Empty image: " + args[0]); System.exit(0); } Mat hsv = new Mat(); Imgproc.cvtColor(src, hsv, Imgproc.COLOR_BGR2HSV); hue = new Mat(hsv.size(), hsv.depth()); Core.mixChannels(Arrays.asList(hsv), Arrays.asList(hue), new MatOfInt(0, 0)); // Create and set up the window. frame = new JFrame("Back Projection 1 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); } 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("* Hue bins: ")); JSlider slider = new JSlider(0, MAX_SLIDER, bins); slider.setMajorTickSpacing(25); slider.setMinorTickSpacing(5); slider.setPaintTicks(true); slider.setPaintLabels(true); slider.addChangeListener(new ChangeListener() {<!-- --> @Override public void stateChanged(ChangeEvent e) {<!-- --> JSlider source = (JSlider) e.getSource(); bins = source.getValue(); update(); } }); sliderPanel.add(slider); pane.add(sliderPanel, BorderLayout.PAGE_START); JPanel imgPanel = new JPanel(); imgLabel = new JLabel(new ImageIcon(img)); imgPanel.add(imgLabel); backprojLabel = new JLabel(); imgPanel.add(backprojLabel); histImgLabel = new JLabel(); imgPanel.add(histImgLabel); pane.add(imgPanel, BorderLayout.CENTER); } private void update() {<!-- --> int histSize = Math.max(bins, 2); float[] hueRange = {<!-- -->0, 180}; Mat hist = new Mat(); List<Mat> hueList = Arrays.asList(hue); Imgproc.calcHist(hueList, new MatOfInt(0), new Mat(), hist, new MatOfInt(histSize), new MatOfFloat(hueRange), false); Core.normalize(hist, hist, 0, 255, Core.NORM_MINMAX); Mat backproj = new Mat(); Imgproc.calcBackProject(hueList, new MatOfInt(0), hist, backproj, new MatOfFloat(hueRange), 1); Image backprojImg = HighGui.toBufferedImage(backproj); backprojLabel.setIcon(new ImageIcon(backprojImg)); int w = 400, h = 400; int binW = (int) Math.round((double) w / histSize); histImg = Mat.zeros(h, w, CvType.CV_8UC3); float[] histData = new float[(int) (hist.total() * hist.channels())]; hist.get(0, 0, histData); for (int i = 0; i < bins; i + + ) {<!-- --> Imgproc.rectangle(histImg, new Point(i * binW, h), new Point((i + 1) * binW, h - Math.round(histData[i] * h / 255.0)), new Scalar(0, 0, 255), Imgproc.FILLED); } Image histImage = HighGui.toBufferedImage(histImg); histImgLabel.setIcon(new ImageIcon(histImage)); frame.repaint(); frame.pack(); } } public class CalcBackProjectDemo1 {<!-- --> 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 CalcBackProject1(args); } }); } }
Python
- Downloadable code:
- Click here for the basic version (explained in this tutorial).
- For more advanced stuff (defining masks for skin areas using H-S histograms and floodFill), check out the improved demo
- …or you can check out the classic camshiftdemo in the samples.
- Code Overview
from __future__ import print_function from __future__ import division import cv2 as cv import numpy as np import argparse def Hist_and_Backproj(val): bins = val histSize = max(bins, 2) ranges = [0, 180] # hue_range hist = cv.calcHist([hue], [0], None, [histSize], ranges, accumulate=False) cv.normalize(hist, hist, alpha=0, beta=255, norm_type=cv.NORM_MINMAX) backproj = cv.calcBackProject([hue], [0], hist, ranges, scale=1) cv.imshow('BackProj', backproj) w = 400 h = 400 bin_w = int(round(w / histSize)) histImg = np.zeros((h, w, 3), dtype=np.uint8) for i in range(bins): cv.rectangle(histImg, (i*bin_w, h), ( (i + 1)*bin_w, h - int(np.round( hist[i]*h/255.0 )) ), (0, 0, 255) , cv.FILLED) cv.imshow('Histogram', histImg) parser = argparse.ArgumentParser(description='Code for Back Projection tutorial.') parser.add_argument('--input', help='Path to input image.', default='home.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) hsv = cv.cvtColor(src, cv.COLOR_BGR2HSV) ch = (0, 0) hue = np.empty(hsv.shape, hsv.dtype) cv.mixChannels([hsv], [hue], ch) window_image = 'Source image' cv.namedWindow(window_image) bins = 25 cv.createTrackbar('* Hue bins: ', window_image, bins, 180, Hist_and_Backproj ) Hist_and_Backproj(bins) cv.imshow(window_image, src) cv.waitKey()
Description
- Read input image
C++
CommandLineParser parser( argc, argv, "{@input |Back_Projection_Theory0.jpg| input image}" ); samples::addSamplesDataSearchSubDirectory("doc/tutorials/imgproc/histograms/back_projection/images"); 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; }
Java
if (args.length != 1) {<!-- --> System.err.println("You must supply one argument that corresponds to the path to the image."); System.exit(0); } Mat src = Imgcodecs.imread(args[0]); if (src.empty()) {<!-- --> System.err.println("Empty image: " + args[0]); System.exit(0); }
Python
parser = argparse.ArgumentParser(description='Code for Back Projection tutorial.') parser.add_argument('--input', help='Path to input image.', default='home.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 it to HSV format:
C++
Mat hsv; cvtColor(src, hsv, COLOR_BGR2HSV);
Java
Mat hsv = new Mat(); Imgproc.cvtColor(src, hsv, Imgproc.COLOR_BGR2HSV);
Python
hsv = cv.cvtColor(src, cv.COLOR_BGR2HSV)
- In this tutorial we will plot a 1D histogram using only tonal values (if you want to use a more standard H-S histogram, check out the more complex code in the link above which will produce better results):
C++
hue.create(hsv.size(), hsv.depth()); int ch[] = {<!-- --> 0, 0 }; mixChannels( & amp;hsv, 1, & amp;hue, 1, ch, 1 );
Java
hue = new Mat(hsv.size(), hsv.depth()); Core.mixChannels(Arrays.asList(hsv), Arrays.asList(hue), new MatOfInt(0, 0));
Python
ch = (0, 0) hue = np.empty(hsv.shape, hsv.dtype) cv.mixChannels([hsv], [hue], ch)
- As you can see, we use the function cv::mixChannels to get only channel 0 (hue) from the hsv image. It gets the following parameters:
- & amp;hsv: source array to copy channels from
- 1: The number of source arrays
- & amp;hue: Copy the target array of the channel
- 1: The number of target arrays
- ch[] = {0,0}: An array of index pairs indicating how to copy the channel. In this example, &hsv ‘s hue (0) channel is copied to &hue ‘s 0-channel (1-channel).
- 1: Number of index pairs
- Create a Trackbar for users to enter score values. Any change on the Trackbar means a call to the Hist_and_Backproj callback function.
C++
const char* window_image = "Source image"; namedWindow(window_image); createTrackbar("* Hue bins: ", window_image, & amp;bins, 180, Hist_and_Backproj ); Hist_and_Backproj(0, 0);
Java
JPanel sliderPanel = new JPanel(); sliderPanel.setLayout(new BoxLayout(sliderPanel, BoxLayout.PAGE_AXIS)); sliderPanel.add(new JLabel("* Hue bins: ")); JSlider slider = new JSlider(0, MAX_SLIDER, bins); slider.setMajorTickSpacing(25); slider.setMinorTickSpacing(5); slider.setPaintTicks(true); slider.setPaintLabels(true); slider.addChangeListener(new ChangeListener() {<!-- --> @Override public void stateChanged(ChangeEvent e) {<!-- --> JSlider source = (JSlider) e.getSource(); bins = source.getValue(); update(); } }); sliderPanel.add(slider); pane.add(sliderPanel, BorderLayout.PAGE_START);
Python
window_image = 'Source image' cv.namedWindow(window_image) bins = 25 cv.createTrackbar('* Hue bins: ', window_image, bins, 180, Hist_and_Backproj ) Hist_and_Backproj(bins)
- Display the image and wait for the user to exit the program:
C++
imshow( window_image, src ); // Wait until user exits the program waitKey();
Java
// Use the content pane's default BorderLayout. No need for // setLayout(new BorderLayout()); // Display the window. frame.pack(); frame.setVisible(true);
Python
cv.imshow(window_image, src) cv.waitKey()
- Hist_and_Backproj Function: Initializes the parameters required by cv::calcHist. The number of partitions comes from Trackbar:
C++
int histSize = MAX(bins, 2); float hue_range[] = {<!-- --> 0, 180 }; const float* ranges[] = {<!-- --> hue_range };
Java
int histSize = Math.max(bins, 2); float[] hueRange = {<!-- -->0, 180};
Python
bins = val histSize = max(bins, 2) ranges = [0, 180] # hue_range
- Calculate the histogram and normalize it to the [0,255] range
C++
calcHist( & amp;hue, 1, 0, Mat(), hist, 1, & amp;histSize, ranges, true, false ); normalize( hist, hist, 0, 255, NORM_MINMAX, -1, Mat() );
Java
Mat hist = new Mat(); List<Mat> hueList = Arrays.asList(hue); Imgproc.calcHist(hueList, new MatOfInt(0), new Mat(), hist, new MatOfInt(histSize), new MatOfFloat(hueRange), false); Core.normalize(hist, hist, 0, 255, Core.NORM_MINMAX);
Python
hist = cv.calcHist([hue], [0], None, [histSize], ranges, accumulate=False) cv.normalize(hist, hist, alpha=0, beta=255, norm_type=cv.NORM_MINMAX)
- Call the function cv::calcBackProject to obtain the back projection result of the same image
C++
Mat backproj; calcBackProject( & amp;hue, 1, 0, hist, backproj, ranges, 1, true );
Java
Mat backproj = new Mat(); Imgproc.calcBackProject(hueList, new MatOfInt(0), hist, backproj, new MatOfFloat(hueRange), 1);
Python
backproj = cv.calcBackProject([hue], [0], hist, ranges, scale=1)
- All parameters are known (the same ones used when calculating the histogram), except that we add the backproj matrix, which will store the backprojection result of the source image ( & amp;hue)
- Show backproj:
C++
imshow( "BackProj", backproj );
Java
Image backprojImg = HighGui.toBufferedImage(backproj); backprojLabel.setIcon(new ImageIcon(backprojImg));
Python
cv.imshow('BackProj', backproj)
- Plot a one-dimensional hue histogram of an image:
C++
int w = 400, h = 400; int bin_w = cvRound( (double) w / histSize ); Mat histImg = Mat::zeros( h, w, CV_8UC3 ); for (int i = 0; i < bins; i + + ) {<!-- --> rectangle( histImg, Point( i*bin_w, h ), Point( (i + 1)*bin_w, h - cvRound( hist.at<float>(i)*h/255.0 ) ), Scalar( 0, 0, 255 ), FILLED ); } imshow( "Histogram", histImg );
Java
int w = 400, h = 400; int binW = (int) Math.round((double) w / histSize); histImg = Mat.zeros(h, w, CvType.CV_8UC3); float[] histData = new float[(int) (hist.total() * hist.channels())]; hist.get(0, 0, histData); for (int i = 0; i < bins; i + + ) {<!-- --> Imgproc.rectangle(histImg, new Point(i * binW, h), new Point((i + 1) * binW, h - Math.round(histData[i] * h / 255.0)), new Scalar(0, 0, 255), Imgproc.FILLED); } Image histImage = HighGui.toBufferedImage(histImg); histImgLabel.setIcon(new ImageIcon(histImage));
Python
w = 400 h = 400 bin_w = int(round(w / histSize)) histImg = np.zeros((h, w, 3), dtype=np.uint8) for i in range(bins): cv.rectangle(histImg, (i*bin_w, h), ( (i + 1)*bin_w, h - int(np.round( hist[i]*h/255.0 )) ), (0, 0, 255) , cv.FILLED) cv.imshow('Histogram', histImg)
Results
Below is the output using a sample image (guess what? the other hand). You can adjust the binary value as you like and see how it affects the result:
R0
R1
R2