本人正在学习 OpenCV 计算机视觉相关知识,此篇为个人学习笔记,参考书籍为冯振等人的《OpenCV4 快速入门》,主要记录 OpenCV 在 Linux 环境下的编译、运行实操流程与相关知识点总结。
笔记内容均为亲自实操验证,过程中踩过不少编译配置、运行参数的坑,整理出来一方面用于自己复盘巩固,另一方面也希望能帮助到刚入门 OpenCV、在编译运行示例程序时遇到问题的小伙伴。文中内容如有疏漏或错误之处,欢迎各位大佬评论区指正交流,共同进步~
第四章 图像直方图与模板匹配
4.1 图像直方图绘制
定义:统计图像的每个灰度值的个数,将图像灰度值作为横轴,以灰度值个数或比率作为纵轴
图像直方图统计函数calcHist()函数
void cv::calcHist(1待统计直方图,2输入的图像数量,3需要统计的通道索引数,4可选的操作掩码,
5输出的统计直方图结果,6需要计算直方图的维度,7存放每个维度直方图的尺寸,
8每个通道中灰度值的取值范围,9直方图是否均匀标志符,10是否累计直方图的标志)
//1.尺寸和类型要相同,类型为cv_8u,cv_16u,cv_32f
//5.是一个dims维度的数组
//6.必须是整数 9.默认是true
直方图计算参数
// 2. 直方图计算参数(通用)
const int channels[1] = { 0 }; // 灰度图仅0通道
float inRanges[2] = { 0,255 }; // 像素值范围0-255
const float* ranges[1] = { inRanges };
const int bins[1] = { 256 }; // 256个bin(对应每个像素值)
四舍五入取整函数cvRound()函数
示例:
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat img = imread("apple.jpg");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat gray;
cvtColor(img, gray, COLOR_BGR2GRAY);
//设置提取直方图的相关变量
Mat hist; //用于存放直方图计算结果
const int channels[1] = { 0 }; //通道索引
float inRanges[2] = { 0,255 };
const float* ranges[1] = { inRanges }; //像素灰度值范围
const int bins[1] = { 256 }; //直方图的维度,其实就是像素灰度值的最大值
calcHist(&gray, 1, channels, Mat(), hist, 1, bins, ranges); //计算图像直方图
//准备绘制直方图
int hist_w = 512;
int hist_h = 400;
int width = 2;
Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3);
for (int i = 1; i <= hist.rows; i++)
{
rectangle(histImage, Point(width*(i - 1), hist_h - 1),
Point(width*i - 1, hist_h - cvRound(hist.at<float>(i - 1) / 15)),
Scalar(255, 255, 255), -1);
}
namedWindow("histImage", WINDOW_AUTOSIZE);
imshow("histImage", histImage);
imshow("gray", gray);
waitKey(0);
return 0;
}
4.2 直方图操作
4.2.1 直方图归一化
方法:寻找统计结果的最大值,将所有结果除以最大值,实现数据归一化0-1
归一化函数:normalize()函数
void cv::normalize(src(输入数组矩阵),dst(输入与src相同大小矩阵),
归一化到下限边界的标准值,上限范围,
归一化标志参数(表4-1),输出数据类型选择标志(为负数,类型相同),掩码矩阵)

示例:
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
system("color F0"); //更改输出界面颜色
vector<double> positiveData = { 2.0, 8.0, 10.0 };
vector<double> normalized_L1, normalized_L2, normalized_Inf, normalized_L2SQR;
//测试不同归一化方法
normalize(positiveData, normalized_L1, 1.0, 0.0, NORM_L1); //绝对值求和归一化
cout << "normalized_L1=[" << normalized_L1[0] << ", "
<< normalized_L1[1] << ", " << normalized_L1[2] << "]" << endl;
normalize(positiveData, normalized_L2, 1.0, 0.0, NORM_L2); //模长归一化
cout << "normalized_L2=[" << normalized_L2[0] << ", "
<< normalized_L2[1] << ", " << normalized_L2[2] << "]" << endl;
normalize(positiveData, normalized_Inf, 1.0, 0.0, NORM_INF); //最大值归一化
cout << "normalized_Inf=[" << normalized_Inf[0] << ", "
<< normalized_Inf[1] << ", " << normalized_Inf[2] << "]" << endl;
normalize(positiveData, normalized_L2SQR, 1.0, 0.0, NORM_MINMAX); //偏移归一化
cout << "normalized_MINMAX=[" << normalized_L2SQR[0] << ", "
<< normalized_L2SQR[1] << ", " << normalized_L2SQR[2] << "]" << endl;
//将图像直方图归一化
Mat img = imread("apple.jpg");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat gray, hist;
cvtColor(img, gray, COLOR_BGR2GRAY);
const int channels[1] = { 0 };
float inRanges[2] = { 0,255 };
const float* ranges[1] = { inRanges };
const int bins[1] = { 256 };
calcHist(&gray, 1, channels, Mat(), hist, 1, bins, ranges);
int hist_w = 512;
int hist_h = 400;
int width = 2;
Mat histImage_L1 = Mat::zeros(hist_h, hist_w, CV_8UC3);
Mat histImage_Inf = Mat::zeros(hist_h, hist_w, CV_8UC3);
Mat hist_L1, hist_Inf;
normalize(hist, hist_L1, 1, 0, NORM_L1, -1, Mat());
for (int i = 1; i <= hist_L1.rows; i++)
{
rectangle(histImage_L1, Point(width*(i - 1), hist_h - 1),
Point(width*i - 1, hist_h - cvRound(30 * hist_h*hist_L1.at<float>(i - 1)) - 1),
Scalar(255, 255, 255), -1);
}
normalize(hist, hist_Inf, 1, 0, NORM_INF, -1, Mat());
for (int i = 1; i <= hist_Inf.rows; i++)
{
rectangle(histImage_Inf, Point(width*(i - 1), hist_h - 1),
Point(width*i - 1, hist_h - cvRound(hist_h*hist_Inf.at<float>(i - 1)) - 1),
Scalar(255, 255, 255), -1);
}
imshow("histImage_L1", histImage_L1);
imshow("histImage_Inf", histImage_Inf);
waitKey(0);
return 0;
}
4.2.2 直方图比较 compareHist()函数
double cv::compareHist(第一幅直方图,第二幅,比较方法标志)

比较方法标志参数:
0:完全一致,计算值为1;完全不相关,计算值为0
1:安全一致,计算值为0;相似性越小,计算值越大
2:两个完全一致的直方图,来自于不同图像,也会有不同数值。数值越大,相似性越高
3:安全一致,计算值为0;相似性越小,计算值越大
4:同3
5:安全一致,计算值为0;相似性越小,计算值越大
示例:
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
void drawHist(Mat &hist, int type, string name) //归一化并绘制直方图函数
{
int hist_w = 512;
int hist_h = 400;
int width = 2;
Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3);
normalize(hist, hist, 1, 0, type, -1, Mat());
for (int i = 1; i <= hist.rows; i++)
{
rectangle(histImage, Point(width*(i - 1), hist_h - 1),
Point(width*i - 1, hist_h - cvRound(hist_h*hist.at<float>(i - 1)) - 1),
Scalar(255, 255, 255), -1);
}
imshow(name, histImage);
}
//主函数
int main()
{
system("color F0"); //更改输出界面颜色
Mat img = imread("apple.jpg");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat gray, hist, gray2, hist2, gray3, hist3;
cvtColor(img, gray, COLOR_BGR2GRAY);
resize(gray, gray2, Size(), 0.5, 0.5);
gray3 = imread("lena.png", IMREAD_GRAYSCALE);
const int channels[1] = { 0 };
float inRanges[2] = { 0,255 };
const float* ranges[1] = { inRanges };
const int bins[1] = { 256 };
calcHist(&gray, 1, channels, Mat(), hist, 1, bins, ranges);
calcHist(&gray2, 1, channels, Mat(), hist2, 1, bins, ranges);
calcHist(&gray3, 1, channels, Mat(), hist3, 1, bins, ranges);
drawHist(hist, NORM_INF, "hist");
drawHist(hist2, NORM_INF, "hist2");
drawHist(hist3, NORM_INF, "hist3");
//原图直方图与原图直方图的相关系数
double hist_hist = compareHist(hist, hist, HISTCMP_CORREL);
cout << "apple_apple=" << hist_hist << endl;
//原图直方图与缩小原图直方图的相关系数
double hist_hist2 = compareHist(hist, hist2, HISTCMP_CORREL);
cout << "apple_apple256=" << hist_hist2 << endl;
//两张不同图像直方图相关系数
double hist_hist3 = compareHist(hist, hist3, HISTCMP_CORREL);
cout << "apple_lena=" << hist_hist3 << endl;
waitKey(0);
return 0;
}
4.3 直方图应用
4.3.1 直方图均衡化equalizeHist()函数
只能对单通道的灰度图进行直方图均衡化
void cv::equalizeHist(src,dst)
//src:需要cv_8uc1图像
示例:
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
void drawHist(Mat &hist, int type, string name) //归一化并绘制直方图函数
{
int hist_w = 512;
int hist_h = 400;
int width = 2;
Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3);
normalize(hist, hist, 1, 0, type, -1, Mat());
for (int i = 1; i <= hist.rows; i++)
{
rectangle(histImage, Point(width*(i - 1), hist_h - 1),
Point(width*i - 1, hist_h - cvRound(hist_h*hist.at<float>(i - 1)) - 1),
Scalar(255, 255, 255), -1);
}
imshow(name, histImage);
}
//主函数
int main()
{
Mat img = imread("gearwheel.jpg");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat gray, hist, hist2;
cvtColor(img, gray, COLOR_BGR2GRAY);
Mat equalImg;
equalizeHist(gray, equalImg); //将图像直方图均衡化
const int channels[1] = { 0 };
float inRanges[2] = { 0,255 };
const float* ranges[1] = { inRanges };
const int bins[1] = { 256 };
calcHist(&gray, 1, channels, Mat(), hist, 1, bins, ranges);
calcHist(&equalImg, 1, channels, Mat(), hist2, 1, bins, ranges);
drawHist(hist, NORM_INF, "hist");
drawHist(hist2, NORM_INF, "hist2");
imshow("原图", gray);
imshow("均衡化后的图像", equalImg);
waitKey(0);
return 0;
}
4.3.2 直方图匹配
定义:将直方图映射成指定的分布形式
直方图匹配过程:
- 步骤 1:计算目标图像的灰度直方图(Hist_src)
|
灰度级 |
出现次数(原图) |
概率(次数 / 总像素) |
|
0 |
2 |
2/8 = 0.25 |
|
1 |
3 |
3/8 = 0.375 |
|
2 |
2 |
2/8 = 0.25 |
|
3 |
1 |
1/8 = 0.125 |
- 步骤 2:计算目标图像的累积分布函数(CDF_src)
|
灰度级 |
CDF_src |
|
0 |
0.25 |
|
1 |
0.25 + 0.375 = 0.625 |
|
2 |
0.625 + 0.25 = 0.875 |
|
3 |
0.875 + 0.125 = 1.0 |
- 步骤 3:计算参考图像的灰度直方图(Hist_ref)和累积分布函数(CDF_ref)
|
类别 |
灰度级0 |
灰度级1 |
灰度级2 |
灰度级3 |
|
出现次数(参考图) |
1 |
2 |
3 |
2 |
|
概率 |
0.125 |
0.25 |
0.375 |
0.25 |
|
CDF_ref |
0.125 |
0.375 |
0.75 |
1.0 |
- 步骤 4:建立灰度映射表(核心!)
映射规则:找到参考图 CDF 中与原图 CDF 最接近的值,将原图的灰度级映射到该值对应的参考图灰度级。逐个匹配原图的 CDF 值:
原图灰度 0 → CDF=0.25 → 参考图 CDF 中最接近的是 0.375(对应灰度 1)→ 映射:0→1
原图灰度 1 → CDF=0.625 → 参考图 CDF 中最接近的是 0.75(对应灰度 2)→ 映射:1→2
原图灰度 2 → CDF=0.875 → 参考图 CDF 中最接近的是 1.0(对应灰度 3)→ 映射:2→3
原图灰度 3 → CDF=1.0 → 参考图 CDF 中最接近的是 1.0(对应灰度 3)→ 映射:3→3
最终映射表:[1,2,3,3](索引对应原图灰度级,值对应目标灰度级)。
- 步骤 5:应用映射表到原图
用映射表替换原图的每个像素:
原图像素:[0,0,1,1,1,2,2,3]
替换后:[1,1,2,2,2,3,3,3]
示例:
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
void drawHist(Mat &hist, int type, string name) //归一化并绘制直方图函数
{
int hist_w = 512;
int hist_h = 400;
int width = 2;
Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3);
normalize(hist, hist, 1, 0, type, -1, Mat());
for (int i = 1; i <= hist.rows; i++)
{
rectangle(histImage, Point(width*(i - 1), hist_h - 1),
Point(width*i - 1, hist_h - cvRound(20 * hist_h*hist.at<float>(i - 1)) - 1),
Scalar(255, 255, 255), -1);
}
imshow(name, histImage);
}
//主函数
int main()
{
Mat img1 = imread("histMatch.png");
Mat img2 = imread("equalLena.png");
if (img1.empty() || img2.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat hist1, hist2;
//计算两张图像直方图
const int channels[1] = { 0 };
float inRanges[2] = { 0,255 };
const float* ranges[1] = { inRanges };
const int bins[1] = { 256 };
calcHist(&img1, 1, channels, Mat(), hist1, 1, bins, ranges);
calcHist(&img2, 1, channels, Mat(), hist2, 1, bins, ranges);
//归一化两张图像的直方图
drawHist(hist1, NORM_L1, "hist1");
drawHist(hist2, NORM_L1, "hist2");
//计算两张图像直方图的累积概率
float hist1_cdf[256] = { hist1.at<float>(0) };
float hist2_cdf[256] = { hist2.at<float>(0) };
for (int i = 1; i < 256; i++)
{
hist1_cdf[i] = hist1_cdf[i - 1] + hist1.at<float>(i);
hist2_cdf[i] = hist2_cdf[i - 1] + hist2.at<float>(i);
}
//构建累积概率误差矩阵
float diff_cdf[256][256];
for (int i = 0; i < 256; i++)
{
for (int j = 0; j < 256; j++)
{
diff_cdf[i][j] = fabs(hist1_cdf[i] - hist2_cdf[j]);
}
}
//生成LUT映射表
Mat lut(1, 256, CV_8U);
for (int i = 0; i < 256; i++)
{
// 查找源灰度级为i的映射灰度
// 和i的累积概率差值最小的规定化灰度
float min = diff_cdf[i][0];
int index = 0;
//寻找累积概率误差矩阵中每一行中的最小值
for (int j = 1; j < 256; j++)
{
if (min > diff_cdf[i][j])
{
min = diff_cdf[i][j];
index = j;
}
}
lut.at<uchar>(i) = (uchar)index;
}
Mat result, hist3;
LUT(img1, lut, result);
imshow("待匹配图像", img1);
imshow("匹配的模板图像", img2);
imshow("直方图匹配结果", result);
calcHist(&result, 1, channels, Mat(), hist3, 1, bins, ranges);
drawHist(hist3, NORM_L1, "hist3"); //绘制匹配后的图像直方图
waitKey(0);
return 0;
}
4.3.3 直方图反向投影 calaBackProject()函数
定义:记录给定图像的像素点如何适应直方图模型像素分布方式的一种方法。就是计算某一特征的直方图模型,然后使用该模型去寻找图像中是否存在该特征的方法。
步骤:1.加载模板图形和待反向投影的图像
2.转换图像颜色空间 3.计算模板图像的直方图 4. 将待反向投影图像赋值给反向投影函数
示例:
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
void drawHist(Mat &hist, int type, string name) //归一化并绘制直方图函数
{
int hist_w = 512;
int hist_h = 400;
int width = 2;
Mat histImage = Mat::zeros(hist_h, hist_w, CV_8UC3);
normalize(hist, hist, 255, 0, type, -1, Mat());
namedWindow(name, WINDOW_NORMAL);
imshow(name, hist);
}
//主函数
int main()
{
Mat img = imread("apple.jpg");
Mat sub_img = imread("sub_apple.jpg");
Mat img_HSV, sub_HSV, hist, hist2;
if (img.empty() || sub_img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
imshow("img", img);
imshow("sub_img", sub_img);
//转成HSV空间,提取S、V两个通道
cvtColor(img, img_HSV, COLOR_BGR2HSV);
cvtColor(sub_img, sub_HSV, COLOR_BGR2HSV);
int h_bins = 32; int s_bins = 32;
int histSize[] = { h_bins, s_bins };
//H通道值的范围由0到179
float h_ranges[] = { 0, 180 };
//S通道值的范围由0到255
float s_ranges[] = { 0, 256 };
const float* ranges[] = { h_ranges, s_ranges }; //每个通道的范围
int channels[] = { 0, 1 }; //统计的通道索引
//绘制H-S二维直方图
calcHist(&sub_HSV, 1, channels, Mat(), hist, 2, histSize, ranges, true, false);
drawHist(hist, NORM_INF, "hist"); //直方图归一化并绘制直方图
Mat backproj;
calcBackProject(&img_HSV, 1, channels, hist, backproj, ranges, 1.0); //直方图反向投影
imshow("反向投影后结果", backproj);
waitKey(0);
return 0;
}
4.4 图像的模板匹配 matchTemplate()函数
定义:用于在一副图像中特定的任务中,同时支持灰度和彩色图像
void cv::matchTemplate(待模板匹配图像,模板图像,输出图像,模板匹配方法标志,匹配模板的掩码)

可选择标志参数:
0:利用平方差,完全匹配,计算值为0;匹配度越低,计算值越大
1:将平方差法进行归一化,完全匹配,计算值为0;匹配度越低,计算值越大
2:乘法操作,数值越大匹配越好;0表示最坏的匹配结果
3:将相关匹配法进行归一化,完全匹配,计算值为1;完全不匹配,计算值为0
4:将相关匹配法对模板减去均值的结果和原始图像减去均值的结果进行匹配。匹配度越高,计算值越大
5:将系数匹配法进行归一化,完全匹配,计算值为1;完全不匹配,计算值为-1
示例:(彩色图像利用标志参数5)
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat img = imread("lena.png");
Mat temp = imread("lena_face.png");
if (img.empty() || temp.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat result;
matchTemplate(img, temp, result, TM_CCOEFF_NORMED);//模板匹配
double maxVal, minVal;
Point minLoc, maxLoc;
//寻找匹配结果中的最大值和最小值以及坐标位置
minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc);
//绘制最佳匹配区域
rectangle(img, cv::Rect(maxLoc.x, maxLoc.y, temp.cols, temp.rows), Scalar(0, 0, 255), 2);
imshow("原图", img);
imshow("模板图像", temp);
imshow("result", result);
waitKey(0);
return 0;
}
4.5 总结

至此,第四章就结束了,望各位读者“一键三连”,再次感谢读到这里的各位读者。有任何问题欢迎评论区留言或者私信。
2056

被折叠的 条评论
为什么被折叠?



