图像处理:直方图——反向投影 OpenCV v4.8.0

上一个教程直方图比较

下一个教程模板匹配

原作者Ana Huamán
兼容性OpenCV >= 3.0

目标

在本教程中,你将学习

理论

什么是反向投影?

  • 反向投影是一种记录给定图像的像素与直方图模型中像素分布的匹配程度的方法。
  • 简单来说 对于背投影,您需要计算某个特征的直方图模型,然后用它在图像中找到该特征。
  • 应用举例: 如果你有一个肉色直方图(例如色相-饱和度直方图),那么你就可以用它来查找图像中的肉色区域:

它是如何工作的?

  • 我们以皮肤为例进行说明:
  • 比方说,你根据下图得到了一个皮肤直方图(色相-饱和度)。除此之外的直方图将是我们的模型直方图(我们知道它代表了皮肤色调的样本)。你应用了一些遮罩,只捕捉皮肤区域的直方图:
    在这里插入图片描述
T0

在这里插入图片描述

T1
  • 现在,让我们想象一下,您得到的另一张手部图像(测试图像)如下: (及其各自的直方图):
    在这里插入图片描述
T2

在这里插入图片描述

T3
  • 我们要做的就是使用我们的模型直方图(我们知道它代表了肤色)来检测测试图像中的皮肤区域。具体步骤如下
  1. 在测试图像的每个像素中(即 p(i,j) ),收集数据并找到该像素对应的 bin 位置(即 (h[i,j],s[i,j]) )。
  2. 在对应的 bin - (h[i,j],s[i,j]) - 中查找模型直方图并读取 bin 值。
  3. 将该二进制值存储到新图像中(BackProjection)。此外,您可以考虑先将模型直方图归一化,这样您就可以看到测试图像的输出。
  4. 通过上述步骤,我们可以得到以下测试图像的 BackProjection 图像:

在这里插入图片描述

  1. 就统计数据而言,存储在 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)
 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)

结果

下面是使用样本图像(猜猜是什么? 另一只手)输出的结果。你可以随意调整二进制值,观察它对结果的影响:

在这里插入图片描述

R0

在这里插入图片描述

R1

在这里插入图片描述

R2
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值