上一个教程 : 直方图比较
下一个教程 : 模板匹配
原作者 | Ana Huamán |
---|---|
兼容性 | OpenCV >= 3.0 |
目标
在本教程中,你将学习
- 什么是反向投影及其用处
- 如何使用 OpenCV 函数 cv::calcBackProject 计算反向投影
- 如何使用 OpenCV 函数 cv::mixChannels 混合图像的不同通道
理论
什么是反向投影?
- 反向投影是一种记录给定图像的像素与直方图模型中像素分布的匹配程度的方法。
- 简单来说 对于背投影,您需要计算某个特征的直方图模型,然后用它在图像中找到该特征。
- 应用举例: 如果你有一个肉色直方图(例如色相-饱和度直方图),那么你就可以用它来查找图像中的肉色区域:
它是如何工作的?
- 我们以皮肤为例进行说明:
- 比方说,你根据下图得到了一个皮肤直方图(色相-饱和度)。除此之外的直方图将是我们的模型直方图(我们知道它代表了皮肤色调的样本)。你应用了一些遮罩,只捕捉皮肤区域的直方图:
- 现在,让我们想象一下,您得到的另一张手部图像(测试图像)如下: (及其各自的直方图):
- 我们要做的就是使用我们的模型直方图(我们知道它代表了肤色)来检测测试图像中的皮肤区域。具体步骤如下
- 在测试图像的每个像素中(即 p(i,j) ),收集数据并找到该像素对应的 bin 位置(即 (h[i,j],s[i,j]) )。
- 在对应的 bin - (h[i,j],s[i,j]) - 中查找模型直方图并读取 bin 值。
- 将该二进制值存储到新图像中(BackProjection)。此外,您可以考虑先将模型直方图归一化,这样您就可以看到测试图像的输出。
- 通过上述步骤,我们可以得到以下测试图像的 BackProjection 图像:
- 就统计数据而言,存储在 BackProjection 中的值表示测试图像中的像素属于皮肤区域的概率,其依据是我们使用的模型直方图。例如,在我们的测试图像中,亮度较高的区域更有可能是皮肤区域(实际情况也是如此),而暗色区域的概率较低(注意这些 "暗色 "区域属于有一些阴影的表面,这反过来会影响检测结果)。
代码
- 这个程序要做什么?
- 加载图像
- 将原始图像转换为 HSV 格式,并仅将色调通道分离出来用于直方图(使用 OpenCV 函数 cv::mixChannels)
- 让用户输入计算直方图时使用的分区数。
- 计算同一图像的直方图(并在分集变化时更新直方图)和背投影。
- 在窗口中显示背投影和直方图。
C++
- 可下载代码:
- 点击此处获取基本版本(在本教程中解释)。
- 如需更高级的内容(使用 H-S 直方图和 floodFill 为皮肤区域定义遮罩),可查看改进版演示
- …或者您也可以查看样本中的经典 camshiftdemo。
- 代码一览
#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!\n" << 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( &hsv, 1, &hue, 1, ch, 1 );
const char* window_image = "Source image";
namedWindow( window_image );
createTrackbar("* Hue bins: ", window_image, &bins, 180, Hist_and_Backproj );
Hist_and_Backproj(0, 0);
imshow( window_image, src );
// 等待直到用户退出程序
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( &hue, 1, 0, Mat(), hist, 1, &histSize, ranges, true, false );
normalize( hist, hist, 0, 255, NORM_MINMAX, -1, Mat() );
Mat backproj;
calcBackProject( &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
- 可下载代码:
- 点击此处获取基本版本(在本教程中解释)。
- 如需更高级的内容(使用 H-S 直方图和 floodFill 为皮肤区域定义遮罩),可查看改进版演示
- …或者您也可以查看样本中的经典 camshiftdemo。
- 代码一览
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));
// 创建并设置窗口。
frame = new JFrame("Back Projection 1 demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 设置内容窗格。
Image img = HighGui.toBufferedImage(src);
addComponentsToPane(frame.getContentPane(), img);
// 使用内容窗格的默认边框布局。无需
// setLayout(new BorderLayout());
// 显示窗口。
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) {
// 加载本地 OpenCV 库
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
// 为事件派发线程安排任务:
// 创建并显示此应用程序的图形用户界面。
javax.swing.SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new CalcBackProject1(args);
}
});
}
}
Python
- 可下载代码:
- 点击此处获取基本版本(在本教程中解释)。
- 如需更高级的内容(使用 H-S 直方图和 floodFill 为皮肤区域定义遮罩),可查看改进版演示
- …或者您也可以查看样本中的经典 camshiftdemo。
- 代码一览
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()
说明
- 读取输入图片
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!\n" << 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)
- 将其转换为 HSV 格式:
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)
- 在本教程中,我们将只使用色调值来绘制一维直方图(如果您想使用更标准的 H-S 直方图,请查看上面链接中更复杂的代码,它能产生更好的效果):
C++
hue.create(hsv.size(), hsv.depth());
int ch[] = { 0, 0 };
mixChannels( &hsv, 1, &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)
- 如你所见,我们使用函数 cv::mixChannels 从 hsv 图像中只获取通道 0(色调)。它获取以下参数:
- &hsv:从中复制通道的源数组
- 1:源数组的个数
- &hue: 复制通道的目标数组
- 1:目标数组的个数
- ch[] = {0,0}: 表示如何复制通道的索引对数组。在本例中,&hsv 的色调(0)通道被复制到 &hue 的 0 通道(1-通道)。
- 1:索引对的数量
- 创建一个 Trackbar 供用户输入分数值。Trackbar 上的任何变化都意味着对 Hist_and_Backproj 回调函数的调用。
C++
const char* window_image = "Source image";
namedWindow( window_image );
createTrackbar("* Hue bins: ", window_image, &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)
- 显示图像并等待用户退出程序:
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 函数: 初始化 cv::calcHist 所需的参数。分区数来自 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
- 计算直方图并将其归一化为 [0,255] 范围
C++
calcHist( &hue, 1, 0, Mat(), hist, 1, &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)
- 调用函数 cv::calcBackProject 获取同一图像的背投影结果
C++
Mat backproj;
calcBackProject( &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)
- 所有参数都是已知的(与计算直方图时使用的参数相同),只是我们添加了 backproj 矩阵,该矩阵将存储源图像 (&hue) 的背投影结果
- 显示 backproj:
C++
imshow( "BackProj", backproj );
Java
Image backprojImg = HighGui.toBufferedImage(backproj);
backprojLabel.setIcon(new ImageIcon(backprojImg));
Python
cv.imshow('BackProj', backproj)
- 绘制图像的一维色调直方图:
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)
结果
下面是使用样本图像(猜猜是什么? 另一只手)输出的结果。你可以随意调整二进制值,观察它对结果的影响: