目录
[Mat 对象创建与赋值](#Mat 对象创建与赋值)
[1. 浅拷贝(共享数据)](#1. 浅拷贝(共享数据))
[2. 深拷贝(复制数据)](#2. 深拷贝(复制数据))
[Haar 级联分类器到底是什么?](#Haar 级联分类器到底是什么?)
OpenCV简介
OpenCV(Open Source Computer Vision Library) 是一套跨平台、开源免费的计算机视觉库,核心定位是「让开发者用简单的 API 快速实现图像 / 视频处理、特征提取、目标检测等计算机视觉任务」,也是目前工业界和学术界最主流的计算机视觉工具之一。
核心特点
-
开源免费 + 跨平台
- 基于 BSD 协议,可商用、可二次开发,无版权限制;
- 支持 Windows、Linux、macOS、Android、iOS 等主流系统,也支持 C++、Python、Java 等编程语言(核心是 C++ 实现)。
-
轻量高效 + 易用性强
- 底层优化(汇编 / 多核并行),处理速度快,适合实时场景(如摄像头实时检测);
- 封装了上万种现成的视觉算法,无需从零实现(比如一行代码完成图像模糊、人脸检测)。
-
生态丰富 + 应用广泛
- 集成了机器学习(如 SVM、KNN)、深度学习(如 DNN 模块支持加载 TensorFlow/PyTorch 模型);
- 社区活跃,中文 / 英文教程、问题解答资源充足。
应用场景
- 日常开发 / 学习:图像裁剪、调色、模糊、视频剪辑、鼠标框选 ROI(就是你学的框选截取功能);
- 计算机视觉基础:目标检测(人脸 / 车牌)、图像分割、特征匹配、运动跟踪;
- 工业 / 商业场景:安防监控、自动驾驶视觉感知、医疗影像分析、OCR 文字识别、无人机视觉;
- 深度学习配套:作为数据预处理工具(图像归一化、缩放),或加载轻量化模型做端侧推理。
OpenCV 是计算机视觉的 "瑞士军刀" ------ 不用造轮子,直接用现成的工具完成从简单的图像显示到复杂的人脸检测的所有视觉任务,也是入门计算机视觉的首选工具。
OpenCV基础入门
图像读取,显示与保存
OpenCV 中图像的读取、显示、保存是最基础的三大核心操作,
对应
imread()、imshow()、imwrite()三个函数
imread()

imshow()

imwrite()

代码示例:
通过以上函数的认识,我们就能够写出第一个opencv函数
cpp
void pro1(Mat& mat) //图像的读取,显示,保存
{
if (mat.empty())
{
std::cout << "图像读取失败...." << std::endl;
return;
}
imwrite("D:\\Temp\\gaomu_new.jpg", mat);
namedWindow("显示图片", WINDOW_FREERATIO);//WINDOW_FREERATIO设置为自由窗口
imshow("显示图片", mat);
waitKey(0);//设置窗口关闭时间ms,0为阻塞
destroyAllWindows();//销毁所有窗口
};
图像色彩空间
色彩空间(Color Space) 是一种用数值表示颜色的数学模型。不同的色彩空间从不同角度描述颜色,适用于不同的图像处理任务。OpenCV中有三种常用的色彩空间BGR,GARY,HSV.
色彩空间也可互相转换(cv::cvtColor)
BGR

GRAY

HSV

cv::cvtColor()

常用转换代码(以BGR为源为例):
COLOR_BGR2GRAY:BGR转灰度COLOR_BGR2HSV:BGR转HSV
代码示例:
cpp
void pro2(Mat& mat) //图像色彩空间的转换(bgr,hsv,gray)
{
imshow("bgr",mat);
Mat m1, m2;
cvtColor(mat, m1, COLOR_BGR2HSV);
cvtColor(mat, m2, COLOR_BGR2GRAY);
imshow("bgr->hsv", m1);
imshow("brg->gray", m2);
waitKey(0);
destroyAllWindows();
};

Mat 对象创建与赋值
前两节我们都用到了Mat对象,下面我们来详解一下Mat的结构
Mat本质上是一个矩阵类,用于存储图像或其他多维数据。它由两部分组成:
矩阵头(header):包含矩阵的尺寸(行数、列数)、通道数、数据类型、一个指向数据的指针以及一个引用计数器等元信息。矩阵头的大小是固定的,占用的内存非常小。
数据体(data):存储实际的像素值或矩阵元素。这块内存的大小取决于图像的尺寸、通道数和数据类型,可能非常庞大。
赋值与拷贝
1. 浅拷贝(共享数据)
cpp
Mat a = imread("image.jpg");
Mat b = a; // 拷贝构造函数:b与a共享数据
Mat c(a); // 同上
Mat d;
d = a; // 赋值运算符:d与a共享数据
2. 深拷贝(复制数据)
cpp
Mat e = a.clone(); // 完全复制一份新数据
Mat f;
a.copyTo(f); // 将a的数据拷贝到f中(f会重新分配内存)
代码示例:
cpp
void pro3()//图像的创建与赋值
{
Mat mat = Mat::zeros(Size(8, 8), CV_8UC3);//cv_8uc3:一个 8 位无符号整数类型的 3 通道矩阵
//scalar用来表示标量 / 向量的类,本质是一个最多 4 个元素的数组,
//专门用来给图像像素赋值。(调用了运算符重载)
mat = Scalar(6, 6, 6);
std::cout << mat << std::endl;
Mat m1 = mat;//浅拷贝
Mat m2, m3;
mat.copyTo(m2);//深拷贝
m3 = mat.clone();//深拷贝
};
图像像素读写
如何遍历和修改每个像素点的数值,分为单通道和多通道。访问模式模式也有两种。
第一种是数组访问模式,用最常规的数组下标访问像素值。
代码示例:
cpp
void pro4(Mat& mat)//图像的读写操作
{
imshow("mat",mat);
int h = mat.rows;//行
int w = mat.cols;//列
int channels = mat.channels();//通道数
/*获取像素值也可通过指针
uchar * m = mat.ptr<uchar>(h);//获取行指针
m++获取每一列的元素这里不演示
*/
for (int i = 0; i < h; i++)
{
for (int j = 0; j < w; j++)
{
if (channels == 1)//gray
{
uchar& num = mat.at<uchar>(i, j);
//printf("num ");
num = 255 - num;//颜色反转
}
else if (channels == 3)//gbr或者hsv
{
uchar& n1 = mat.at<Vec3b>(i, j)[0];
uchar& n2 = mat.at<Vec3b>(i, j)[1];
uchar& n3 = mat.at<Vec3b>(i, j)[2];
//printf("%d,%d,%d ",n1,n2,n3);
//颜色反转
n1 = 255 - n1;
n2 = 255 - n2;
n3 = 255 - n3;
}
}
std::cout << std::endl;
}
imshow("像素读写演示", mat);
waitKey(0);
};

图像像素算术操作
对图像的各个像素点实现加减乘除的操作。介绍了常用的除爆函数saturate_cast,防止数值过界。
cpp
Mat m = mat + Scalar(50,50,50);
Mat m = mat - Scalar(50, 50, 50);
Mat m = mat * Scalar(5, 5, 5);
Mat m = mat / Scalar(50, 50, 50);
eg:以上这些操作看似正确,但存在着隐患(uchar 类型,取值范围 0~255)(像素数据类型溢出 + 运算规则 ).
opencv中为我们封装了算术函数,超出范围时自动截断或者对单个像素操作时可以用saturate_case<uchar>(row,col)来限制范围
代码示例:
cpp
void pro5(Mat& mat)//像素的算数操作
{
//add(mat, Scalar(50, 50, 50), m);
//subtract(mat, Scalar(2, 2, 2), m);
//multiply(mat,Scalar(2,2,2),m);
//divide(mat, Scalar(10, 10, 10), m);
imshow("div", mat);
waitKey(0);
}
//或
void pro5(Mat& image)
{
int dims = image.channels();
int h = image.rows;
int w = image.cols;
for (int row = 0; row < h; row++)
{
for (int col = 0; col < w; col++)
{
Vec3b p1 = image.at<Vec3b>(row, col); //opencv特定的类型,获取三维颜色,3个值
Vec3b p2 = m.at<Vec3b>(row, col);
dst.at<Vec3b>(row, col)[0] = saturate_cast<uchar>(p1[0] + p2[0]);//saturate_cast用来防爆,小于0就是0,大于255就是255
dst.at<Vec3b>(row, col)[1] = saturate_cast<uchar>(p1[1] + p2[1]);
dst.at<Vec3b>(row, col)[2] = saturate_cast<uchar>(p1[2] + p2[2]);//对彩色图像读取它的像素值,并且对像素值进行改写。
}
}
}
滑动条调整图像亮度与对比度
**滑动条(Trackbar)**是 OpenCV 高 GUI 模块的一部分,它可以附加到指定的窗口上,并关联一个整型变量。当滑块位置改变时,系统会调用一个回调函数,你可以在其中根据滑块的值更新图像。
OpenCV 的滑动条回调可以看作是一种轻量级的"Qt信号-槽"简化版,专为快速图像处理演示和简单交互而设计。


代码示例:
cpp
//注意:我的函数是放在自定义的一个类中,Mat m为类的公有属性
//亮度调节进度条
static void lightbar(int pos, void* data)
{
OpenCV* test = static_cast<OpenCV*>(data);
Mat dstM;
add(test->m, Scalar(pos, pos, pos), dstM);
imshow("亮度调节", dstM);
}
//对比度调节进度条
static void contrastbar(int pos, void* data)
{
OpenCV* test = static_cast<OpenCV*>(data);
Mat m0 = Mat::zeros(test->m.size(), test->m.type());
Mat dstM;
double contrast = pos / 100.0;//计算权重1.0-2.0
//!!addWeighted按权重加法函数
addWeighted(test->m, contrast, m0, 0.0, pos, dstM);
imshow("亮度调节", dstM);
}
void pro6(Mat& mat)
{
m = mat;
int lightness = 20;
int contrast = 100;
namedWindow("亮度调节", WINDOW_FREERATIO);
//(滚动条名,窗口名,亮度值,最大值,回调函数,回调参数)
createTrackbar("light", "亮度调节", &lightness, 100, lightbar, this);
createTrackbar("contrast", "亮度调节", &contrast, 200, contrastbar, this);
//初始化设置
lightbar(lightness, this);
contrastbar(contrast, this);
waitKey(0);
}

颜色表操作
applyColorMap 是 OpenCV 中用于将灰度图像映射为彩色图像的函数。其原型为:
void applyColorMap(InputArray src, OutputArray dst, int colormap);
这些颜色映射将灰度值(0~255)线性映射到特定的 RGB 颜色空间,常用于科学可视化(如热力图、深度图、红外图像等),以增强人眼对灰度差异的感知。
代码示例:
cpp
//颜色表操作
void pro7(Mat& mat)
{
int colormap[]
{
COLORMAP_AUTUMN,
COLORMAP_BONE,
COLORMAP_CIVIDIS,
COLORMAP_COOL,
COLORMAP_DEEPGREEN,
COLORMAP_HOT,
COLORMAP_HSV,
COLORMAP_INFERNO,
COLORMAP_JET,
COLORMAP_MAGMA,
COLORMAP_OCEAN,
COLORMAP_PARULA,
COLORMAP_PINK,
COLORMAP_PLASMA,
COLORMAP_RAINBOW,
COLORMAP_SPRING,
COLORMAP_SUMMER,
COLORMAP_TURBO,
COLORMAP_TWILIGHT,
COLORMAP_TWILIGHT_SHIFTED,
COLORMAP_VIRIDIS,
COLORMAP_WINTER
};
int index = 0;
while (1)
{
Mat m;
if (27 == waitKey(100))
{
std::cout << "ESC..." << std::endl;
exit(0);
}
applyColorMap(mat, m, colormap[(index++) % 19]);
imshow("show", m);
}
}
图像像素的逻辑操作
本节介绍如何对图像的像素进行操作,包括与、或、非、异或,矩形在图像中的绘制。
cpp
void pro8()//图像像素的逻辑操作(与,或,非,异或)
{
//空图像
Mat m1 = Mat::zeros(Size(256, 256), CV_8UC3);
Mat m2 = Mat::zeros(Size(256, 256), CV_8UC3);
//绘制图像(-1表示填充矩形,1代表绘制框)
rectangle(m1, Rect(100, 100, 80, 80), Scalar(255, 255, 0), -1, LINE_8, 0);
rectangle(m2, Rect(150, 150, 80, 80), Scalar(0, 255, 255), -1, LINE_8, 0);
imshow("m1", m1);
imshow("m2", m2);
Mat dst;
// bitwise_and(m1, m2, dst); // 与运算:仅重叠区域保留(青+黄=绿)
// bitwise_or(m1, m2, dst); // 或运算:两个矩形区域都保留(青+黄合并)
// bitwise_not(m1, dst); // 非运算:对m1取反(青色变反色,背景变黑)
bitwise_xor(m1, m2, dst); // 异或运算:非重叠区域保留,重叠区域清零
imshow("m1m2", dst);
waitKey(0);
destroyAllWindows();
}

通道分离与合并



cpp
void pro9(Mat& mat) //通道的分离与合并
{
imshow("before", mat);
std::vector<Mat>mv;
split(mat, mv);//通道分离
//单通道图片
imshow("g",mv[0]);
imshow("b",mv[1]);
imshow("r",mv[0]);
//通道合并
Mat dst;
merge(mv, dst);
imshow("merge", dst);
//通道交换
Mat dst1 = Mat::zeros(mat.size(), mat.type());
int fromTo[] = { 0,1 ,1,0 ,2,2 };
mixChannels(&mat, 1, &dst1, 1, fromTo, 3);
imshow("jiao huan", dst1);
waitKey(0);
destroyAllWindows();
}

图像色彩空间转换
本节内容实现任务是提取纯色背景人物的轮廓实现纯色背景的切换,首先把RGB色彩空间的图片转换到HSV空间中,其次通过使用inrangle根据HSV的颜色范围提取人物遮罩的mask,通过copyTo来实现切换

cpp
void pro10(Mat& mat)//图像色彩空间的转换
{
Mat hsv;
//BGR转HSV(HSV空间更适合颜色筛选)
cvtColor(mat, hsv, COLOR_BGR2HSV);
//定义蓝色的HSV范围,生成掩码(mask中蓝色区域为255,其余为0)
Mat mask;
inRange(hsv, Scalar(100, 43, 46), Scalar(124, 255, 255), mask);
imshow("mask", mask);
//生成红色背景
Mat redback = Mat::zeros(mat.size(), mat.type());
redback = Scalar(40, 40, 200);
//蓝色区域为255,非蓝色为0
mask = ~mask;
imshow("~mask", mask);
//255区域替换为红色,0的区域替换为原图
mat.copyTo(redback, mask);
imshow("redback", redback);
waitKey(0);
destroyAllWindows();
}
图像的像素值统计
cpp
void pro11(Mat& mat) //图像的像素值统计(最大值,最小值,平均值,标准差)
{//minMaxLoc仅支持单通道
std::vector<Mat>mv;
split(mat, mv);
for (int i = 0; i < mv.size(); i++)
{
double vmin, vmax;
Point pmin, pmax;
cv::minMaxLoc(mv[i], &vmin, &vmax, &pmin, &pmax);
std::cout << i << ":max:" << vmax << "maxpoint" << pmax << std::endl;
std::cout << i << ":min:" << vmin << "minpoint" << pmin << std::endl;
}
Mat mean, stdDev;
meanStdDev(mat, mean, stdDev);
std::cout << "mean:" << mean << "stdDev" << stdDev << std::endl;
}

图像几何形状绘制
本节课介绍如何绘制椭圆,矩形,直线,圆等
cpp
void QuickDemo::drawing_demo(Mat &image)
{
Rect rect;
rect.x = 200;
rect.y = 200;
rect.width = 100;
rect.height = 100;
Mat bg = Mat::zeros(image.size(),image.type());
rectangle(image, rect, Scalar(0, 0, 255), -1, 8, 0);
//参数1为绘图的底图或者画布名称,参数2位图片的起始,宽度,高度
//参数3代表填充颜色。参数4大于0是线小于0是填充
//参数5表示邻域填充,参数6默认值为0
circle(bg, Point(350, 400), 15, Scalar(0, 0, 255), 2, LINE_AA, 0);
//参数2位图片中心位置,参数3为半径为15的圆
Mat dst;
//addWeighted(image, 0.7, bg, 0.3, 0, dst);
RotatedRect rtt;
rtt.center = Point(200, 200);
rtt.size = Size(100, 200);
rtt.angle = 0.0;
line(bg,Point(100,100),Point(350,400), Scalar(0, 0, 255), 8, LINE_AA, 0);//line_AA表示去掉锯齿
ellipse(bg,rtt, Scalar(0, 0, 255), 2, 8);
imshow("矩形的绘制",bg);
}
随机数与随机颜色
本节主要介绍如何能产生一个随机数字和随机颜色,并且用线条的方式显示出来。
1.用当前时间 time(NULL) 作为种子初始化 RNG 对象 rng,确保每次运行产生不同的随机序列。
2.使用 rng.uniform(min,max) 随机生成
cpp
void pro13() //随机数与随机颜色
{
Mat mat = Mat::zeros(Size(256, 256), CV_8UC3);
RNG rng(time(NULL));
while (1)
{
Mat dst = Mat::zeros(Size(256, 256), CV_8UC3);
mat = dst.clone();
int x = rng.uniform(0, mat.cols);
int y = rng.uniform(0, mat.rows);
int x1 = rng.uniform(0, mat.cols);
int y1 = rng.uniform(0, mat.rows);
Point p1(x, y); // 起点
Point p2(x1, y1); // 终点
// 生成随机颜色(BGR格式,0~255)
Scalar random_color(
rng.uniform(0, 256),
rng.uniform(0, 256),
rng.uniform(0, 256)
);
line(mat, p1, p2, random_color, 1, LINE_8);
dst = mat.clone();
imshow("pro13", dst);
int key = waitKey(30);
if (key == 27) break; // 退出循环
}
destroyAllWindows();
}
多边形填充与绘制



cpp
void pro14(Mat& mat)//多边形的填充与绘制(fillPoly、polylines、drawContours)
{
std::vector<Point>pt, pt1;
Point p1(100, 100);
Point p2(90, 90);
Point p3(60, 150);
Point p4(80, 200);
Point p5(130, 200);
pt.push_back(p1);
pt.push_back(p2);
pt.push_back(p3);
pt.push_back(p4);
pt.push_back(p5);
Point p1_1(100 + 120, 100);
Point p2_1(90 + 120, 90);
Point p3_1(60 + 120, 150);
Point p4_1(80 + 120, 200);
Point p5_1(130 + 120, 200);
pt1.push_back(p1_1);
pt1.push_back(p2_1);
pt1.push_back(p3_1);
pt1.push_back(p4_1);
pt1.push_back(p5_1);
//pts二维结构
std::vector<std::vector<Point>>pts;
pts.push_back(pt);
pts.push_back(pt1);
//polylines(mat,pts,true,Scalar(0,0,255),3,LINE_8);
fillPoly(mat, pts, Scalar(255, 255, 0), LINE_8);
//drawContours(mat,pts,1, Scalar(255, 255, 0),2);
imshow("pro14", mat);
waitKey(0);
destroyAllWindows();
}
鼠标交互与框选
cpp
//pro15鼠标的操作与响应
static Point p1, p2;
static void onMouse(int event, int x, int y, int flags, void* userdata)
{
Mat mat = *(Mat*)(userdata);
Mat m = mat.clone();
if (event == EVENT_LBUTTONDOWN)//左键按下
{
p1 = Point(x, y);
}
else if (event == EVENT_MOUSEMOVE && (flags & EVENT_FLAG_LBUTTON))//左键按下并移动
{
p2 = Point(x, y);
Mat tem = m.clone();
rectangle(tem, p1, p2, Scalar(255, 0, 0), 2);
imshow("mouse", tem);
}
else if (event == EVENT_LBUTTONUP)//左键抬起
{
p2 = Point(x, y);
rectangle(m, p1, p2, Scalar(255, 0, 0), 2);
//防止超出范围
if (p2.x >= m.cols) p2.x = m.cols - 1;
if (p2.y >= m.rows) p2.y = m.rows - 1;
Rect roi(min(p1.x, p2.x), min(p1.y, p2.y), abs(p1.x - p2.x), abs(p1.y - p2.y));
if (roi.width <= 0 || roi.height <= 0) return;
imshow("roi截取区域", Mat(m(roi)));
imshow("mouse", m);
}
}
void pro15(Mat& mat)
{
imshow("mouse", mat);
setMouseCallback("mouse",onMouse,(void*)(&mat));
waitKey(0);
destroyAllWindows();
}

像素类型转换与归一化



cpp
void pro16(Mat& mat)//图像的像素类型与归一化
{
imshow("before",mat);
std::cout << "before mat.type:" << mat.type() << std::endl;
mat.convertTo(mat,CV_32F);//转换为浮点型
//为什么转换为浮点型后,是全白?(归一化)
imshow("after",mat);
std::cout << "after mat.type:" << mat.type() << std::endl;
Mat dst;
//NORM_MINMAX最常用
normalize(mat,dst,1.0,0.0,NORM_MINMAX);//归一化(四种归一化方式)
imshow("gui",dst);
waitKey(0);
destroyAllWindows();
}

图像的放缩与差值
介绍基本的图像变换大小的方法。
图像的差值处理主要有线性、双线性差值、卢卡斯差值、双立方差值。


cpp
void pro17(Mat& mat) //图像缩放
{
imshow("before",mat);
Mat dst;
Size size(800,400);
resize(mat,dst,size, INTER_LINEAR);
imshow("resize", dst);
waitKey(0);
destroyAllWindows();
}

图像的旋转
图像的旋转需要两个函数


简单的旋转代码示例:
cpp
void pro19(Mat& mat)//图像的旋转
{
Mat m = mat.clone();
int h = m.rows;
int w = m.cols;
int angle = 45;
Mat can = getRotationMatrix2D(Point(w/2,h/2),angle,1);
warpAffine(m, m, can, m.size());
imshow("旋转", m);
waitKey(0);
destroyAllWindows();
}
但我们运行是发现,旋转后超出画框部分的图像消失了这是什么原因?

这是因为 **
warpAffine变换时输出图像尺寸和画布范围没有调整 **,导致旋转后超出原始图像边界的像素被直接裁剪掉了。
我们可以根据下面这个博主的图片从新计算画布的宽高,以及旋转中心

cpp
void pro19(Mat& mat)//图像的旋转
{
Mat m = mat.clone();
int h = m.rows;
int w = m.cols;
int angle = 45;
Mat can = getRotationMatrix2D(Point(w/2,h/2),angle,1);
// 计算旋转后的新尺寸(关键:避免裁剪)
// 步骤1:将角度转为弧度
double rad = angle * CV_PI / 180.0;
// 步骤2:计算旋转后图像的宽/高(包含所有像素)
int new_w = abs(w * cos(rad)) + abs(h * sin(rad));
int new_h = abs(w * sin(rad)) + abs(h * cos(rad));
// 调整平移量:让旋转后的图像居中显示在新尺寸中
can.at<double>(0, 2) += (new_w - w) / 2.0;
can.at<double>(1, 2) += (new_h - h) / 2.0;
Mat imat;
warpAffine(m,imat,can, Size(new_w, new_h));
Mat redback = Mat::zeros(imat.size(), imat.type());
redback = Scalar(40, 40, 200); //红色背景
// 6. 制作蒙版(二值化,只保留旋转后图像的有效区域)
Mat gray;
cvtColor(imat, gray, COLOR_BGR2GRAY); // 转灰度图
Mat mask;
//inrange也可
threshold(gray, mask, 1, 255, THRESH_BINARY); // 二值化:非黑区域设为255(白)
// 7. 图像融合:将旋转后的图像复制到红色背景上(只显示有效区域)
imat.copyTo(redback, mask);
imshow("旋转", redback);
waitKey(0);
destroyAllWindows();
}
图像的翻转
图像的翻转就简单多了,OpenCV为我们提供了专用的函数filp

cpp
void pro18(Mat& mat) //图像的翻转
{
rectangle(mat,Rect(40,40,50,50),Scalar(255,255,0),-1);
imshow("原",mat);
flip(mat,mat,0);
imshow("上下", mat);
flip(mat, mat, 1);
imshow("左右", mat);
flip(mat, mat, -1);
imshow("对角线", mat);
waitKey(0);
destroyAllWindows();
}
注意:由于我使用的是原图像,所以每一次操作都是基于上一次操作后的图像

视频读取与摄像头
cpp
void pro20()
{
// 1. 初始化:打开默认摄像头(索引0)也可传入文件地址打开MP4
VideoCapture video(0);
// 2. 循环读取帧
while (true)
{
if (27 == waitKey(10))break;
Mat frame;
video.read(frame);
flip(frame,frame, 1);
//展示帧
imshow("摄像头画面", frame);
}
video.release();
destroyAllWindows();
}
视频的处理与保存
通过上一节我们知道,其实视频也是图片(连续的图片切换).
这里有一个概念叫做帧率,帧率就是每秒刷新显示多少张图片
下面来学习视频的简单处理,第一来学习如何获取视频参数和修改参数
而视频的处理其实就是借助我们上面学习的对图片的处理方法
在VideoCapture类中get和set函数可以用来获取视频参数和修改参数,重载是通过参数中的宏来实现的
其中常用的宏有:

代码演示:
cpp
void pro21()
{
VideoCapture video("D://temp/testVideo.mp4");
if (!video.isOpened()) return;
//
int h = video.get(CAP_PROP_FRAME_WIDTH);
int w = video.get(CAP_PROP_FRAME_HEIGHT);
int fps = video.get(CAP_PROP_FPS);
int allfps = video.get(CAP_PROP_FRAME_COUNT);
printf("h:%d,w:%d\nfps:fps:%d,allfps:%d",h,w,fps,allfps);
while(true)
{
Mat frame;
bool ret = video.read(frame);
if (!ret || frame.empty())
{
std::cout << "视频播放完毕 / 帧读取失败,平稳退出" << std::endl;
break;
}
cvtColor(frame,frame,COLOR_BGR2GRAY);
imshow("mp4", frame);
if (27 == waitKey(100))break;
}
video.release();
destroyAllWindows();
}
图像的直方图
计算通道的直方图的函数


cpp
void pro22(Mat& mat) //图像的直方图
{
if (mat.channels() != 3) {
std::cout << "请输入彩色图像!" << std::endl;
return;
}
// 1. 分离 B/G/R 三个通道
std::vector<Mat>mv;
split(mat,mv);
const int bin[1] = {256};//像素的区间数
const float rang[2] = {0,256};//区间
const float* rangs[] = {rang};//函数要求二级指针
Mat b_hist;
Mat g_hist;
Mat r_hist;
//2.分别计算三个通道的直方图
calcHist(&mv[0],1,0,Mat(),b_hist,1,bin,rangs);
calcHist(&mv[1], 1, 0, Mat(), g_hist, 1, bin, rangs);
calcHist(&mv[2], 1, 0, Mat(), r_hist, 1, bin, rangs);
// 3. 归一化
double hisHight = 400;
double hisWeight = 512;
normalize(b_hist,b_hist,hisHight,0.0,NORM_MINMAX);
normalize(g_hist, g_hist, hisHight, 0.0, NORM_MINMAX);
normalize(r_hist, r_hist, hisHight, 0.0, NORM_MINMAX);
// 4. 创建画布
Mat hisImg(hisHight, hisWeight, CV_8UC3, Scalar(255, 255, 255));
int binWidth = hisWeight / 256; // 每个灰度级占用2个像素宽度
// 5. 绘制三个通道的直方图(蓝/绿/红对应 B/G/R)
for (int i = 1; i < bin[0]; i++) {
//连接当前点和上一个点
// 蓝色通道(B)
line(hisImg, Point(binWidth * (i - 1), hisHight - cvRound(b_hist.at<float>(i - 1))),
Point(binWidth * i, hisHight - cvRound(b_hist.at<float>(i))), Scalar(255, 0, 0), 2);
// 绿色通道(G)
line(hisImg, Point(binWidth * (i - 1), hisHight - cvRound(g_hist.at<float>(i - 1))),
Point(binWidth * i, hisHight - cvRound(g_hist.at<float>(i))), Scalar(0, 255, 0), 2);
// 红色通道(R)
line(hisImg, Point(binWidth * (i - 1), hisHight - cvRound(r_hist.at<float>(i - 1))),
Point(binWidth * i, hisHight - cvRound(r_hist.at<float>(i))), Scalar(0, 0, 255), 2);
}
imshow("原图",mat);
imshow("直方图",hisImg);
waitKey(0);
destroyAllWindows();
}

二维直方图
上面我们讲解了一维直方图的创建,一维直方图只关注单个通道(比如灰度、红色通道)的像素值分布,而二维直方图会同时统计两个通道的像素值组合分布,这在色彩分析(比如 H-S 通道分析)中特别常用。
它们的创建逻辑高度相似,核心区别只是「统计维度」和「参数设置」。
问题:为什么二维直方图常用 HSV?
简答:RGB 通道混亮度、易受光照干扰;HSV 将色相 H、饱和度 S 与亮度 V 分离,只用 H-S 做二维直方图,颜色特征更稳定、抗光强、区分更准。
cpp
//二维直方图
void pro23(Mat& mat)
{
imshow("原图",mat);
if (mat.channels() != 3) return;
Mat hsv;
cvtColor(mat,hsv,COLOR_BGR2HSV);
//h通道与s通道的最大值不一样
int hbin = 180, sbin = 256;
const int histSize[2] = { hbin,sbin };
const float hrang[2] = {0,180};
const float srang[2] = {0,256};
const float* rangs[2] = {hrang,srang};
int channels[2] = {0,1};
Mat Hist;
calcHist(&hsv,1,channels,Mat(),Hist,2,histSize,rangs,true,false);
normalize(Hist, Hist, 0, 255, NORM_MINMAX, -1, Mat());
// 4. 创建画布并绘制直方图
int hist_h = 400, hist_w = 600; // 画布尺寸
// 用cvRound取整,保证bin尺寸更精准,避免整数除法丢失精度
int h_bin_w = cvRound((float)hist_w / hbin);
int s_bin_h = cvRound((float)hist_h / sbin);
Mat hist_img = Mat::zeros(hist_h, hist_w, CV_8UC3);
// 双重循环遍历H/S bins,绘制矩形
for (int h = 0; h < hbin; h++) {
for (int s = 0; s < sbin; s++) {
float bin_val = Hist.at<float>(h, s); // 获取当前bin的数值
int intensity = cvRound(bin_val); // 归一化后的强度
// 绘制矩形(填充颜色用强度表示)
rectangle(hist_img,
Point(h * h_bin_w, s * s_bin_h),
Point((h + 1) * h_bin_w, (s + 1) * s_bin_h),
Scalar(intensity, intensity, intensity), // 灰度显示
FILLED); // 填充矩形
}
}
applyColorMap(hist_img,hist_img,COLORMAP_JET);
imshow("二维直方图",hist_img);
waitKey(0);
destroyAllWindows();
}

直方图的均衡化
直方图均衡化是 OpenCV 中最常用的图像增强技术 之一,核心目标是拉伸图像的像素值分布范围,让直方图更均匀,从而提升图像的对比度、凸显暗部 / 亮部细节,尤其适合低对比度的图像(比如逆光照片、医学影像)。
cpp
//直方图均衡化()
void pro24(Mat& mat)
{
Mat m = mat.clone();
cvtColor(mat,m,COLOR_BGR2GRAY);
imshow("原图gray",m);
Mat dst;
equalizeHist(m,dst);
imshow("直方图均衡化",dst);
waitKey(0);
destroyAllWindows();
}

图像的卷积操作
什么是卷积?
图像卷积 = 用一个小核(滤波器),在图片上滑动、加权求和,提取特征 / 做处理。

用一张图来解释卷积的过程:


卷积核的选择(核心原则 + 典型案例)
卷积核是卷积的 "规则",选对核 = 实现想要的效果,核心原则:
- 核的大小 :优先选奇数尺寸(3×3、5×5、7×7),方便确定中心点;
- 核的权重和 :
- 滤波 / 平滑类核:权重和≈1(避免图像过亮 / 过暗);
- 边缘检测类核:权重和≈0(边缘像素突出,非边缘趋近 0);
- 核的对称性:多数场景用对称核(如高斯核、Sobel 核),非对称核仅用于特定方向的特征提取。
- 核心:核越大,模糊效果越强,但计算速度越慢,细节丢失越严重。
卷积核中心点的选择(关键规则)
中心点是卷积核的 "锚点",也是计算后新像素对应的位置,核心规则:
- 必须选奇数核 :只有奇数尺寸的核(3×3、5×5)才有唯一的中心点 (3×3 核的中心点是第 2 行第 2 列,5×5 核是第 3 行第 3 列);
- 若用偶数核(2×2、4×4),无明确中心点,会导致图像偏移、计算混乱,工程中几乎不用;
- 中心点权重的意义 :
- 模糊核:中心点权重通常最大(如高斯核中心点是 4/16),让当前像素的贡献最大,避免图像失真;
- 锐化核:中心点权重远大于周边(如 5),强化当前像素与周边的差异,突出细节;
- 中心点与图像对齐:卷积计算时,核的中心点会 "对准" 原图的每个像素,计算结果作为新图对应位置的像素值 ------ 这是保证图像空间位置不偏移的核心
三种模糊算法详解
均值模糊(blur)
原理解读
均值模糊是最基础的模糊算法,核心特点是「卷积核内所有像素权重完全相等」。
- 卷积核:以 3×3 核为例,所有元素值为 1,计算后需除以 9(核的总像素数),保证像素值在 0-255 合法范围;
- 计算逻辑:窗口内所有像素值求和 ÷ 像素总数 → 新像素值。
通俗比喻:把窗口内的像素 "一视同仁",不管是核心像素还是边缘像素,都平均对待,最终得到均匀的模糊效果。
核心特点
-
优点:计算速度最快,实现简单;
-
缺点:模糊效果生硬,会同时糊掉图像边缘(比如物体轮廓、人像五官),细节丢失严重。
-
核心函数:
-
cv2.blur(src, ksize)/blur(src, dst, ksize) -
src:输入图像(支持灰度图 / 彩色图); -
ksize:卷积核大小,必须为正奇数(如 3×3、5×5、9×9),核越大模糊效果越强。
cpp
void pro25(Mat& mat)//图像的卷积操作
{
imshow("原图",mat);
Mat m = mat.clone();
Mat dst;
//Size卷积核大小,Point中心点
blur(m,dst,Size(3,3),Point(-1,-1));
imshow("图像的卷积", dst);
waitKey(0);
destroyAllWindows();
}

高斯模糊
原理解读
高斯模糊解决了均值模糊 "一刀切" 的问题,核心特点是「卷积核权重符合高斯分布(钟形曲线)」:
- 卷积核:中心像素权重最高,离中心越远,权重越低(比如 3×3 高斯核:[[1,2,1],[2,4,2],[1,2,1]]);
- 计算逻辑:窗口内像素值 × 对应权重求和 → 除以权重总和(OpenCV 自动完成)→ 新像素值。
通俗比喻:模糊时 "优先参考离得近的像素,少参考离得远的像素",最终模糊效果更贴近人眼视觉,更自然。
核心特点
- 优点:模糊效果自然,比均值模糊更符合实际需求;
- 缺点:仍会糊掉图像边缘,只是比均值模糊柔和。
cv2.GaussianBlur() 函数
在 OpenCV 中,cv2.GaussianBlur() 用于对图像进行高斯模糊处理,其基本格式如下:
cv2.GaussianBlur(src, ksize, sigmaX,, sigmaY, borderType)
参数说明
src:输入图像(可以是灰度图或彩色图)
ksize:高斯核的大小(通常是奇数,如 (3,3)、(5,5))
sigmaX:X 方向的标准差(控制模糊程度,值越大模糊越强)
sigmaY(可选):Y 方向的标准差,默认情况下 sigmaY=sigmaX
borderType(可选):边界填充方式,默认 cv2.BORDER_DEFAULT
注意:高斯核必须是奇数
为什么呢?如下:
中心对称性:高斯滤波的核心是以中心像素为基准,均匀向四周扩散。如果是偶数,则无法确定一个对称的中心点。
卷积计算方便:高斯核是用于卷积运算的,奇数尺寸可以保证左右上下对称,避免边界偏移问题。
OpenCV 规范:OpenCV 规定 ksize 必须为奇数,确保滤波核的正确性。
cpp
void pro26(Mat& mat) //高斯模糊
{
Mat m = mat.clone();
Mat dst;
GaussianBlur(m, dst, Size(3, 3),15);
imshow("高斯模糊", dst);
}
高斯双边模糊

原理解读
高斯双边模糊是 "进阶版高斯模糊",核心特点是「双重权重约束」:
- 空间域权重:和高斯模糊一致,离中心越近权重越高;
- 值域权重:新增约束,和中心像素灰度 / 颜色差异越小,权重越高。
通俗比喻:模糊时不仅看 "距离近不近",还看 "颜色像不像"------ 只有 "位置近 + 颜色像" 的像素才会参与模糊,颜色差异大的边缘像素会被保留。
这是三种模糊中唯一能「去噪的同时保留边缘」的算法,也是人像磨皮、风景图去噪的核心算法。
核心特点
- 优点:去噪的同时保留图像边缘(比如人像磨皮不糊五官);
- 缺点:计算量最大,速度最慢(比前两种模糊耗时久)。
cpp
void pro27(Mat& mat) //高斯双边模糊
{
Mat m = mat.clone();
Mat dst;
bilateralFilter(m, dst,9,100,10);
imshow("高斯双边模糊", dst);
waitKey(0);
destroyAllWindows();
}

实时人脸检测
Haar 级联分类器到底是什么?
Haar 级联分类器(Haar Cascade Classifier)是 OpenCV 最经典、最稳定、最老牌的人脸检测方法,由 Paul Viola 和 Michael Jones 在 2001 年提出。
它的核心特点:
- 不需要深度学习
- 不需要 GPU
- 不需要下载复杂模型(.pb/.onnx)
- OpenCV 安装自带
- 速度快、体积小、跨平台稳定
它的工作原理:
- 使用 Haar 特征(类似边缘、线条、块特征)
- 通过 积分图加速计算
- 使用 级联分类器 过滤非人脸区域
- 最终快速定位人脸位置
一句话总结:Haar = 传统机器视觉人脸检测,稳定、轻量、不依赖外部文件,最适合学习与入门。
CascadeClassifier faceDetector;
作用:创建一个人脸检测器
- 专门用于 Haar 人脸检测
- 是 Haar 模型的入口工具
faceDetector.load( "haarcascade_frontalface_alt2.xml" );
这个文件在OpenCV的build目录下中的etc的haarcascades中
作用:加载自带的人脸模型
- 不需要自己下载
- 是 OpenCV 官方训练好的正脸检测模型
detectMultiScale(gray, faces,1.1,4);
【核心函数】人脸检测
detectMultiScale 中的 1.1 是图像缩放比例,用于检测不同尺寸的人脸;
4 是最小邻域值,用于过滤错误检测,让结果更干净、更准确。
- 输入:灰度图
- 输出:所有检测到的人脸框
- 是整个程序最关键的函数
cpp
void pro28()
{
// 打开摄像头
VideoCapture cap(0);
if (!cap.isOpened()) {
cout << "摄像头打开失败!" << endl;
return;
}
// OpenCV 自带人脸检测(不需要外部模型)
CascadeClassifier faceDetector;
faceDetector.load("D:/Engineering/opencv/build/etc/haarcascades/haarcascade_frontalface_alt2.xml");
Mat frame;
while (cap.read(frame))
{
Mat gray;
cvtColor(frame, gray, COLOR_BGR2GRAY);
vector<Rect> faces;
faceDetector.detectMultiScale(gray, faces, 1.1, 4);
// 给人脸画框
for (Rect f : faces) {
rectangle(frame, f, Scalar(0, 255, 0), 2);
}
cv::flip(frame,frame,1);
imshow("人脸检测 - pro28", frame);
if (waitKey(10) == 27) break; // 按 ESC 退出
}
}
上面演示的是 OpenCV 自带的 Haar 级联分类器 ,它的优势是轻量、稳定、不需要额外下载模型,非常适合入门学习与快速开发。
但在实际项目、高精度场景中,我们还可以使用更强大的深度学习人脸检测器 。这些模型通常由官方或社区训练好,以 .pb、.pbtxt、.onnx 等格式提供,我们可以直接从 GitHub 下载使用。
