opencv

opencv相关知识

计算机图像颜色基础理论

用一个巧妙的实验来给你讲清楚什么是 Gamma,还未记录总结

RGB颜色空间

基本知识

RGB三原色起源于上世纪初1809年Thomas Young提出视觉的三原色学说,随后Helmholtz在1824年也提出了三原色学说:即:视网膜存在三种视锥细胞,分别含有对红、绿、蓝三种光线敏感的视色素,当一定波长的光线作用于视网膜时,以一定的比例使三种视锥细胞分别产生不同程度的兴奋,这样的信息传至大脑中枢,就产生某一种颜色的感觉。

在显示器发明之后,从黑白显示器发展到彩色显示器,人们开始使用发出不同颜色的光的荧光粉(CRT,等离子体显示器),或者不同颜色的滤色片(LCD),或者不同颜色的半导体发光器件(OLED和LED大型全彩显示牌)来形成色彩,无一例外的选择了Red,Green,Blue这3种颜色的发光体作为基本的发光单元。通过控制他们发光强度,组合出了人眼睛能够感受到的大多数的自然色彩。

计算机显示彩色图像的时候也不例外,最终显示的时候,要控制一个像素中Red,Green,Blue的值,来确定这个像素的颜色。计算机中无法模拟连续的存储从最暗到最亮的量值,而只能以数字的方式表示。于是,结合人眼睛的敏感程度,使用3个字节(3*8位)来分别表示一个像素里面的Red,Green 和Blue的发光强度数值,这就是常见的RGB格式。我们可以打开画图板,在自定义颜色工具框中,输入r,g,b值,得到不同的颜色。

RGB颜色空间以R(Red:红)、G(Green:绿)、B(Blue:蓝)三种基本色为基础,进行不同程度的叠加,产生丰富而广泛的颜色,所以俗称三基色模式。

RGB空间是生活中最常用的一个颜色显示模型,电视机、电脑的CRT显示器等大部分都是采用这种模型。自然界中的任何一种颜色都可以由红、绿、蓝三种色光混合而成,现实生活中人们见到的颜色大多是混合而成的色彩。

组合方法是通过互补光的形式来组合成任意颜色的

image-20240505145338856

三通道想要组合成黑白灰,必须三原色值相同,当某一方的值不相同时就会产生其他颜色。

灰度图不一定是单通道,但是单通道一定是灰度图

将RGB图像转变为灰度图像遵循下面的公式:(仅需了解)
$$
gray = B0.114+G0.587+R*0.299
$$

img

HSV颜色空间

RGB颜色空间的分量与亮度密切相关,即只要改变亮度,3个分量都会随之相应地改变.因此,RGB颜色空间适合于显示系统,却并不适合于图像处理

  • 色调: 表示观察者感知的主要颜色

  • 饱和度: 指的是相对的纯净度,或一种颜色混合白光的数量

    如深红色(红加白)和淡紫色(紫加白)这样的色彩是欠饱和的,饱和度与所加的白光的数量成反比

  • 亮度: 表达了无色的强度概念

image-20240505194825626image-20240505195849731

因为,色调+饱和度 = 色度,所以:
$$
颜色 = 色调+饱和度+亮度 = 色度+亮度
$$
image-20240505195921691

图像的基本类型

在计算机中,我们常见的图像类型有以下几种:

  1. 二值化图像: 一幅二值图像的二维矩阵仅由0、1两个值构成,“0”代表黑色,“1”代白色。由于每一像素(矩阵中每一元素)取值仅有0、1两种可能,所以计算机中二值图像的数据类型通常为1个二进制位。二值图像通常用于文字、线条图的扫描识别(OCR)和掩膜图像的存储。
  2. 灰度图像: 灰度图像矩阵元素的取值范围通常为[0,255]。因此其数据类型一般为8位无符号整数的(int8),这就是人们经常提到的256灰度图像。“0”表示纯黑色,“255”表示纯白色,中间的数字从小到大表示由黑到白的过渡色。在某些软件中,灰度图像也可以用双精度数据类型(double)表示,像素的值域为[0,1],0代表黑色,1代表白色,0到1之间的小数表示不同的灰度等级。二值图像可以看成是灰度图像的一个特例。
  3. **索引图像:**索引图像的文件结构比较复杂,除了存放图像的二维矩阵外,还包括一个称之为颜色索引矩阵MAP的二维数组。MAP的大小由存放图像的矩阵元素值域决定,如矩阵元素值域为[0,255],则MAP矩阵的大小为256Ⅹ3,用MAP=[RGB]表示。MAP中每一行的三个元素分别指定该行对应颜色的红、绿、蓝单色值,MAP中每一行对应图像矩阵像素的一个灰度值,如某一像素的灰度值为64,则该像素就与MAP中的第64行建立了映射关系,该像素在屏幕上的实际颜色由第64行的[RGB]组合决定。也就是说,图像在屏幕上显示时,每一像素的颜色由存放在矩阵中该像素的灰度值作为索引通过检索颜色索引矩阵MAP得到。索引图像的数据类型一般为8位无符号整形(int8),相应索引矩阵MAP的大小为256Ⅹ3,因此一般索引图像只能同时显示256种颜色,但通过改变索引矩阵,颜色的类型可以调整。索引图像的数据类型也可采用双精度浮点型(double)。索引图像一般用于存放色彩要求比较简单的图像,如Windows中色彩构成比较简单的壁纸多采用索引图像存放,如果图像的色彩比较复杂,就要用到RGB真彩色图像。
  4. **真彩色RGB图像:**RGB图像与索引图像一样都可以用来表示彩色图像。与索引图像一样,它分别用红(R)、绿(G)、蓝(B)三原色的组合来表示每个像素的颜色。但与索引图像不同的是,RGB图像每一个像素的颜色值(由RGB三原色表示)直接存放在图像矩阵中,由于每一像素的颜色需由R、G、B三个分量来表示,M、N分别表示图像的行列数,三个M x N的二维矩阵分别表示各个像素的R、G、B三个颜色分量。RGB图像的数据类型一般为8位无符号整形,通常用于表示和存放真彩色图像,当然也可以存放灰度图像。

图像的大小、深度和通道

在OpenCV中,定义的图像一般会包含图像的大小、深度和通道等几个元素。一般我们是用 cv::Mat 去定义一个图像,例如:

1
cv::Mat img(512, 512, CV_8UC3, Scalar(255, 255, 255));

上面这一语句实例化了一个Mat 的对象img;其大小为512像素*512像素,类型为8位无符号整型三通道;颜色为纯白色(Scalar(255,255,255) B= 255 G= 255 R=255)

CV_8UC3 中 8表示 8bit; U表示无符号整型数 S = 符号整型 F = 浮点型; C3 表示3通道;

同理,CV_8SC1 即表示8bit带符号整型单通道类型的图像;CV_32FC2是指一个32位浮点型双通道矩阵,这两个例子中的数字就代表了位深度,数字越大单个通道能表示的颜色就越细,可以理解位调节的粒度。

位深度 取值范围
IPL_DEPTH_8U - 无符号8位整型 0–255
IPL_DEPTH_8S - 有符号8位整型 -128–127
IPL_DEPTH_16U - 无符号16位整型 0–65535
IPL_DEPTH_16S - 有符号16位整型 -32768–32767
IPL_DEPTH_32S - 有符号32位整型 0–65535
IPL_DEPTH_32F - 单精度浮点数 0.0–1.0
IPL_DEPTH_64F - 双精度浮点数 0.0–1.0

那么,单通道和多通道有啥区别呢,例如,单通道图像由一字节就可以表示一个像素的色彩(明暗),而三通道则需要3个字节才能表示一个像素的色彩,RGB彩色图一般就是三通道,每个通道表示一个颜色。4通道通常为RGBA,在某些处理中可能会用到。2通道图像不常见,通常在程序处理中会用到,如傅里叶变换,可能会用到,一个通道为实数,一个通道为虚数,主要是编程方便。还有一种情况就是16位图像,本来是3通道,但是为了减少数据量,压缩为16位,刚好两个通道,常见格式有RGB555或RGB565,也就是说R占5位,G占5或6位,B占5位,也有RGBA5551格式。

图像在计算机中的坐标

图像像素的坐标不同于我们常见的直角坐标,它的坐标原点是在图像的左上角,向右为x的正方向,向下为y轴的正方向;

image-20240505145055851
  • VGA = 640 x 480
  • HD = 1280 ×720
  • FHD = 1920 x 1080
  • 4K = 3840 ×2160

灰度图像:从0~256($2^8$)

image-20240503161327947

对于彩色图像,有三个灰度图像,分别代表红色,绿色和蓝色的强度.将他们合在一起就是完整的彩色图像

image-20240503161719931

opencv环境配置

mac配置

brew install opencvvcpkg install opencv4 (opencv4是记录时的最新版)

[[C++基础#C++如何使用第三方库|参考此处了解cmake应该如何使用]]

opencv常用的头文件

头文件 功能
<opencv2/core.hpp> OpenCV 核心功能,包括基本数据结构和函数
<opencv2/imgproc.hpp> 图像处理功能,如滤波、变换、颜色空间转换等
<opencv2/highgui.hpp> 图形用户界面功能,如 imshow()waitKey()
<opencv2/imgcodecs.hpp> 图像读写功能,如 imread()imwrite()
<opencv2/features2d.hpp> 特征检测和描述功能
<opencv2/calib3d.hpp> 3D 计算机视觉功能,如相机标定、三维重建等
<opencv2/video.hpp> 视频分析功能,如光流、背景建模等
<opencv2/objdetect.hpp> 对象检测功能,如人脸检测、行人检测等
<opencv2/ml.hpp> 机器学习功能
<opencv2/dnn.hpp> 深度学习功能
<opencv2/flann.hpp> 快速近似最近邻搜索功能
<opencv2/photo.hpp> 图像处理功能,如去噪、美化等
<opencv2/stitching.hpp> 图像拼接功能
<opencv2/videoio.hpp> 视频 I/O 功能
1
2
3
4
//最基本,常用:
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

Windows配置

下载位置

CSharp Opencv

使用nuget包OpenCvSharp4

如果是windows上开发,需要运行时包:OpenCvSharp4.runtime.win,要与OpenCvSharp4安装同一个版本

OpenCvSharp4.WpfExtensions:用于在wpf上使用

简单代码案例

读取图片并窗口显示

1
2
3
4
string path = "../../Resources/str.jpeg";
Mat img = imread(path);
imshow("Image",img);
waitKey(0);//堵塞直到键入

读取视频并窗口显示

1
2
3
4
5
6
7
8
9
10
11
//测试视频捕获
string path = "../../Resources/3s.ts";
VideoCapture cap(path);
Mat img;
while(true)
{
cap.read(img);
imshow("Image",img);
waitKey(20); //延迟20毫秒
}
//此代码,视频播放完了会有异常,因为视频播放完了,读不到了

读取摄像头并窗口显示

1
2
3
4
5
6
7
VideoCapture cap(0);//0表示摄像头编号
Mat img;
while(true){
cap.read(img);
imshow("Image",img);
waitKey(1);
}

Mat是opencv引入的矩阵类型(Matrix)

Mat类

OpenCV官方参考文档

image-20240910092328125

结构构成

image-20240910084627340

Mat能存储的数据

cv::Mat

  • cv::Mat_<_TP> 任何类型
  • cv::Mat_<double> 双浮点
  • cv::Mat_<float> 单浮点
  • cv::Mat_<uchar> cv::Mat_<unsigned char> 无符号字符
  • 等等

OpenCV中规定的数据类型

数据类型 具体类型 取值范围
CV_8U 8位无符号整数 0—255
CV_8S 8位符号整数 -128—127
CV_16U 16位无符号整数 0—65535
CV_16S 16位符号整数 -32768—32767
CV_32U 32位无符号整数 0—4294967295
CV_32S 32位符号整数 -2147483648—2147483647
CV_32F 32位浮点整数 -FLT_MAX—FLT_MAX, INF, NAN
CV_64F 64位浮点整数 -DBL_MAX—DBL_MAX, INF, NAN

Mat类的赋值

创建时赋值

1
cv::Mat::Mat(int rows,int cols,int type,const Scalar& s)
  • rows: 矩阵的行数
  • cols: 矩阵的列数
  • type: 存储数据的类型,矩阵中存储的数据类型,此处除了CV_8UC1,CV_64FC4等从1到4通道以外,还提供了更多通道的参数,通过CV_8UC(n)中的n来构建多通道矩阵,其中n最大可以取到512
  • s: 给矩阵中每个像素赋值的参数变量,如Scalar(0,0,255)

利用已有的Mat类创建新的Mat类

1
cv::Mat::Mat(const Mat& m,const Range& rowRange,const Range& colRange=Range::all())
  • m: 已经构建完成的Mat类矩阵数据
  • rowRange: 在已有矩阵中需要截取的行数范围,是一个Range变量,例如从第2行到第5行(不包括第2行与第5行)可以表示为Range(2,5)
  • colRange: 在已有矩阵中需要截取的列数范围,是一个Range变量,例如从第2列到第5列(不包括第2列与第5列)可以表示为Range(2,5),在不输入任何值时表示所有列都会被截取

利用矩阵Size()结构和数据类型参数创建Mat类

1
cv::Mat::Mat(Size size,int type)
  • size: 2D数组变量尺寸,通过Size(cols,rows)进行赋值,如Size(3,3)
  • type: 存储数据的类型

Mat类方法赋值

  • eye: 单位矩阵
  • diag: 对角矩阵
  • ones: 元素全为1的矩阵
  • zeros: 元素全为0的矩阵
1
cv::Mat a=cv::Mat::eye(3,3,type);

枚举法赋值

1
2
cv::Mat a=(cv::Mat_<int>(3,3)<<1,2,3,4,5,6,7,8,9);
cv::Mat b=(cv::Mat_<double>(2,3)<<1.0,2.1,3.2,4.0,5.1,6.2);

Mat类元素的读取

Mat数据在内存中存放形式

image-20240910092715148

常用属性

属性 作用
cols 矩阵的列数(以像素个数为单位)
rows 矩阵的行数(以像素个数为单位)
step 以字节为单位的矩阵的有效宽度
elemSize() 每个元素的字节数
total() 矩阵中元素的个数(总像素个数)
channels() 矩阵的通道数

at方法读取

at(int row,int col)

1
2
3
4
5
//单通道
int value = (int)a.at<uchar>(0,0);
//多通道
cv::Vec3b vc3 = b.at<cv::Vec3b>(0,0);
int first = (int)vc3.val[0];

罗列一些预设数据类型的命名规则

  • Vec3b 三个通道字节字节型
  • Vec2b 两个通道字节型
  • Vec4i 四个通道int型

矩阵元素地址定位方式访问元素

1
2
//单通道
(int)(*(b.data+b.step[0]*row +b.step[1]*col+channel));

Mat支持的运算

直接利用数学符号表示矩阵的运算

1
2
3
4
5
6
//矩阵与数字的运算,都是将运算应用于每个元素
e=a+b;
f=c-d;
g=2*a;
h=d/2.0;
i=a-1;//Mat类减数字,是Mat中每个元素都减去这个数字

两个矩阵相乘

  • 矩阵乘积 * a = b*c;
  • 向量内积 .dot a.dot(b)
  • 对应位元素乘积 .mul() a.mul(b)

[[数学#乘法意义盘点|上述各种乘法的现实意义参考此处]]

相关函数

函数名 作用
absdiff() 两个矩阵对应元素差的绝对值
add() 两个矩阵求和
addWeighted() 两个矩阵线性求和
divide() 矩阵除法
invert() 矩阵求逆
log() 矩阵求对数
max()/min() 两个矩阵计算最大值/最小值.min(a, b) 会返回一个新的矩阵,其中每个元素都是 ab 中对应位置元素的最小值
  • 矩阵的线性求和: 是指将多个矩阵按一定的权重进行求和。具体来说,线性求和涉及到对每个矩阵的元素乘以一个标量(权重),然后再进行求和。

  • 矩阵求和: 是指将两个相同维度的矩阵对应位置的元素相加,得到一个新的矩阵。

  • 矩阵除法: 实际上是A/B = A * B的逆矩阵

    意义

    • 求解线性方程组
    • 矩阵可以用来表示线性变换。矩阵的“除法”可以理解为反向应用这种变换
  • 矩阵对数 略,也非常重要

矩阵运算的理解

基本图像处理操作

灰度/高斯模糊/边缘/扩张/侵蚀

  • 转换为灰度图像
  • 添加高斯模糊
  • canny边缘检测
  • 图像扩张
  • 图像侵蚀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
string path = "../../Resources/str.jpeg";
Mat img = imread(path);
//转为灰度图像
Mat imgGray;
cvtColor(img,imgGray,COLOR_BGR2GRAY);//opencv中将RGB称为BGR
//添加高斯模糊
Mat imgBlur;
GaussianBlur(img,imgBlur,Size(3,3),3,0);
//canny边缘检测,边缘检测前,通常需要做些模糊处理
Mat imgCanny;
Canny(imgBlur,imgCanny,25,75);
//图像扩张(增加厚度)
Mat imgDil;
Mat kernel = getStructuringElement(MORPH_RECT,Size(5,5));//有尽量使用奇数的说法,似乎是会导致图像偏移?
dilate(imgCanny,imgDil,kernel);
//图像侵蚀(减少厚度)
Mat imgErode;
erode(imgDil,imgErode,kernel);
imshow("Image",img);
imshow("Image Gray",imgGray);
imshow("Image Blur",imgBlur);
imshow("Image Canny",imgCanny);
imshow("Image Dilation",imgDil);
imshow("Image Erode",imgErode);
waitKey(0);
DestroyAllWindows();
image-20240505134628409

亮度与对比度

ConvertToConvertScaleAbs 是 OpenCV 中用于图像处理的两个函数,它们的主要区别在于功能和用途。

ConvertTo

  • 功能:用于将图像从一种数据类型转换为另一种数据类型,同时可以应用缩放因子和偏移量。

  • 用法

    1
    cv::convertTo(src, dst, dtype, alpha, beta)

    其中 src 是输入图像,dst 是输出图像,dtype 是目标数据类型,alpha 是缩放因子,beta 是偏移量。

  • 示例:可以将图像从 CV_8U 转换为 CV_32F,并且可以在转换时调整亮度和对比度。

ConvertScaleAbs

  • 功能:用于将图像转换为绝对值并缩放,同时将结果转换为 CV_8U 类型。

  • 用法

    1
    cv::convertScaleAbs(src, dst, alpha, beta)

    其中 src 是输入图像,dst 是输出图像,alpha 是缩放因子,beta 是偏移量。

  • 示例:通常用于将浮点图像转换为 8 位图像,适合于显示和保存。

对于输入图像中的每个像素,函数执行如下转换:
$$
\text{dst}(x, y) = \text{abs}(\alpha \cdot \text{src}(x, y) + \beta)
$$

  • alpha(对比度):如果 alpha > 1,图像的对比度会增加(图像更加清晰);如果 alpha < 1,图像的对比度会减小。
  • beta(亮度):控制增加或减少像素的亮度值。

总结

  • ConvertTo 更加灵活,支持多种数据类型的转换和调整。
  • ConvertScaleAbs 专注于将图像转换为 8 位无符号整数,并且自动处理绝对值,通常用于将处理后的图像准备好进行显示。

调整图像大小/裁剪图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
string path = "../../Resources/str.jpeg";
Mat img = imread(path);
//调整大小
Mat imgResize,imgResize2;
std::cout<<img.size()<<std::endl;//打印图像原本大小 [468 x 265]
resize(img,imgResize,Size(400,200));//缩放为大小:400,200(无视宽高比调整)
resize(img,imgResize2,Size(),0.5,0.5);//0,5倍等比例缩小
//裁切
Mat imgCrop;
Rect roi(100,100,200,50);
imgCrop = img(roi);//裁切区间必须小于图像本身大小,不然会报错
imshow("Image",img);
imshow("Image Resize",imgResize);
imshow("Image Resize2",imgResize2);
imshow("Image Crop",imgCrop);
waitKey(0);
DestroyAllWindows();
image-20240505140243693

绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//新建空白图片,参数解释如下:
//CV_8UC3中 8表示2的8次方,U表示无符号(结合前面就是0~255),C3表示三通道(3 channels)
//Scalar(255,255,255)表示白色
Mat img(512,512,CV_8UC3,Scalar(255,255,255));
//画圆圈 从坐标(256,256)做半斤为155的圆圈,颜色为(0,69,255),线条粗细为10(若为FILLED表示实心圆)(注意这个线条粗细是往两边同时扩张的)
circle(img,Point(256,256),155,Scalar(0,69,255),50);
//画矩形
rectangle(img,Point(101,101),Point(411,411),Scalar(255,0,0),3);
//画线
line(img,Point(130,296),Point(382,296),Scalar(0,0,0),2);
//画文本(不支持中文,将会是乱码)
putText(img,"hello world!",Point(20,262),FONT_HERSHEY_DUPLEX,0.7,Scalar(0,0,0),2);
imshow("Image",img);
waitKey(0);
DestroyAllWindows();
image-20240505143710291

仿射变换

  • 仿射变换是线性变换和平移的组合。
  • 仿射变换保持直线的性质,但不保持原点不变。
  • 仿射变换可以用矩阵乘法和向量加法来表示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
string path = "../../Resources/cards.jpg";
Mat img = imread(path);
float w = 250;
float h = 350;
Mat matrix,imgWarp;//分别用于存储透视变换的矩阵,用于存储变换后的图像

Point2f src[4] ={{520,142},{771,190},{405,395},{674,457}};//定义变换前的矩形
Point2f dst[4] ={{0.0f,0.0f},{w,0.0f},{0.0f,h},{w,h}};//定义变换后的矩形

matrix = getPerspectiveTransform(src,dst);//计算透视变换矩阵matrix
///使用warpPerspective函数,将img中的图像应用透视变换,结果存储在imgWarp中。参数matrix是变换矩阵,Point(w,h)指定输出图像的大小
warpPerspective(img,imgWarp,matrix,Point(w,h));

//画上一些标记点
for (int i = 0; i < 4; i++)
{
circle(img,src[i],10,Scalar(0,0,255),FILLED);
}
imshow("Image",img);
imshow("Image Warp",imgWarp);
DestroyAllWindows();
image-20240505181014198

颜色检测

直方图均衡化

直方图均衡化(Histogram Equalization)是一种用于图像处理的技术,目的是增强图像的对比度,使得图像的亮度分布更加均匀。这种方法尤其适合提升灰度图像的对比度,但也可以扩展到彩色图像。

像的直方图表示图像中不同灰度级(亮度)的像素分布情况。例如,假设一个灰度图像的像素值在 0(黑色)到 255(白色)之间,那么直方图就展示了每个灰度级出现的次数。通过分析直方图,可以判断图像是否过亮、过暗,或对比度过低。

直方图均衡化的核心思想是通过重新分布图像的灰度值,使得像素在所有灰度级上的分布更为均匀,从而增加图像的整体对比度。

工作原理

通过累积分布函数重新分配图像的灰度级,从而均衡亮度并提升细节

  1. 计算直方图:首先,计算图像中每个灰度值的像素数量,得到图像的直方图。

  2. 计算累积分布函数 (CDF):累积分布函数表示某个灰度级及其以下的所有像素值在图像中所占的总比例。计算每个灰度值的累积概率分布。公式如下:
    $$
    CDF(i) = \sum_{j=0}^{i} p(j)
    $$
    其中,p(j) 是第 j 灰度值出现的概率。

  3. 灰度值映射:根据累积分布函数,将每个灰度值映射到一个新的灰度值区间。新的灰度值会是原始值的线性扩展,从而平衡亮度分布。公式为:
    $$
    new_pixel_value = CDF(i) \times (L - 1)
    $$
    其中,L 是灰度级数(例如,对于 8 位图像,L 为 256)。

  4. 生成均衡化后的图像:将所有像素点的灰度值通过映射转换为新的值,得到对比度增强的图像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//实际计算案例参考如下:
假设有一个简单的直方图,灰度值为 03,频率如下:

| 灰度值 | 频率 |
|--------|------|
| 0 | 2 |
| 1 | 3 |
| 2 | 1 |
| 3 | 2 |

1. 计算直方图:
- 直方图已经给出。

2. 计算 CDF
- CDF(0) = 2
- CDF(1) = 2 + 3 = 5
- CDF(2) = 5 + 1 = 6
- CDF(3) = 6 + 2 = 8

3. 归一化 CDF(假设总像素数 N = 8):
- CDF_norm(0) = 2 / 8 = 0.25
- CDF_norm(1) = 5 / 8 = 0.625
- CDF_norm(2) = 6 / 8 = 0.75
- CDF_norm(3) = 8 / 8 = 1.0

4. 映射到新的灰度值(L = 4):
- new_pixel_value(0) = 0.25 * (4 - 1) = 0.75 → 变为 1(取整)
- new_pixel_value(1) = 0.625 * (4 - 1) = 1.875 → 变为 2
- new_pixel_value(2) = 0.75 * (4 - 1) = 2.25 → 变为 2
- new_pixel_value(3) = 1.0 * (4 - 1) = 3.0 → 变为 3

5. 生成新的图像:
- 替换原始灰度值为新灰度值,得到增强后的图像。

经过直方图均衡化处理的图像通常会显得更加清晰,对比度更强。对于暗淡或亮度不均的图像,这种方法非常有效。直方图均衡化可以让更多的细节显现出来,尤其是在光照不足的情况下。直方图均衡化处理使得整个图像的亮度分布更加均匀,提升图像的可视性。

局限性

  • 过度增强:在某些情况下,直方图均衡化可能会导致图像的噪声也被增强,从而影响图像质量。
  • 色彩失真:在处理彩色图像时,若直接对每个颜色通道单独进行均衡化,可能会导致色彩失真。因此,处理彩色图像时通常采用类似的技术,比如自适应直方图均衡化对亮度通道进行均衡化

python案例

在OpenCV中,直方图均衡化可以通过函数 cv2.equalizeHist() 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import cv2
import numpy as np

# 读取灰度图像
img = cv2.imread('image.jpg', 0)

# 进行直方图均衡化
equalized_img = cv2.equalizeHist(img)

# 显示原图与均衡化后的图像
cv2.imshow('Original Image', img)
cv2.imshow('Equalized Image', equalized_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

对于彩色图像,可以先将其转换到 YUV 颜色空间,对亮度通道(Y)进行均衡化,然后再转换回 RGB 颜色空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 读取彩色图像
img = cv2.imread('image.jpg')

# 转换为YUV颜色空间
yuv_img = cv2.cvtColor(img, cv2.COLOR_BGR2YUV)

# 对亮度通道Y进行直方图均衡化
yuv_img[:, :, 0] = cv2.equalizeHist(yuv_img[:, :, 0])

# 转回RGB颜色空间
equalized_img = cv2.cvtColor(yuv_img, cv2.COLOR_YUV2BGR)

# 显示结果
cv2.imshow('Equalized Image', equalized_img)
cv2.waitKey(0)
cv2.destroyAllWindows()

自适应直方图均衡化

全面替代直方图均衡化

自适应直方图均衡化(Adaptive Histogram Equalization, 简称 AHE)是一种改进的直方图均衡化技术,主要用于提升图像局部区域的对比度。在普通的直方图均衡化中,整个图像使用全局直方图进行处理,这可能导致对比度过度增强或者细节丢失,尤其是在对比度变化较大的图像中。自适应直方图均衡化通过对图像的不同区域分别进行均衡化,来解决这些问题。

在 C++ 中,使用 OpenCV 可以通过 CLAHE(Contrast Limited Adaptive Histogram Equalization,对比度限制的自适应直方图均衡化)来实现这种效果。CLAHE 是 AHE 的一种改进,它通过限制对比度增强的强度,防止过度增强。

原理

  1. 分块处理:将图像分成多个小块(通常称为“网格”或“区块”),并对每个小块分别进行直方图均衡化。每个小块的对比度都根据其局部直方图来调整。
  2. 插值:为了避免块与块之间出现明显的边界(块效应),会对相邻块的均衡化结果进行[[算法#插值算法|双线性插值]],使图像变得更平滑。
  3. 对比度限制:在 CLAHE 中,会限制直方图中某个灰度值的最大频率(像素数量),这样可以防止局部区域对比度过高,避免过亮或过暗的区域产生噪声。

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

int main()
{
// 读取灰度图像
cv::Mat src = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);

// 判断图像是否成功加载
if(src.empty())
{
std::cout << "图像加载失败!" << std::endl;
return -1;
}

// 创建一个CLAHE对象
// clipLimit 是对比度限制阈值,tileGridSize 是分块的大小
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(2.0, cv::Size(8, 8));

// 存储均衡化后的结果
cv::Mat dst;

// 应用CLAHE
clahe->apply(src, dst);

// 显示原图和处理后的图像
cv::imshow("Original Image", src);
cv::imshow("CLAHE Image", dst);

// 等待按键
cv::waitKey(0);

return 0;
}

创建 CLAHE 对象

使用 cv::createCLAHE() 函数创建一个 CLAHE 对象。这个函数的两个参数分别是:

  • clipLimit:限制对比度的阈值,通常值在 2 到 4 之间。较大的值会允许更强的对比度增强。
  • tileGridSize:控制分块的大小。较小的块会对局部区域增强更多细节。

应用 CLAHE

clahe->apply() 方法用于对图像进行自适应直方图均衡化,结果存储在 dst 中。

附一个CSharp案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 应用CLAHE
private Mat ApplyCLAHE(Mat input)
{
Mat output = new Mat();

// 转换到LAB颜色空间
Cv2.CvtColor(input, output, ColorConversionCodes.BGR2Lab);

// 分离通道
Mat[] channels = Cv2.Split(output);

// 对L通道应用CLAHE
var clahe = Cv2.CreateCLAHE(2.0, new OpenCvSharp.Size(8, 8));
Mat enhancedL = new Mat();
clahe.Apply(channels[0], enhancedL);

// 创建一个与原始L通道大小相同的Mat
Mat resizedL = new Mat();
Cv2.Resize(enhancedL, resizedL, channels[0].Size(), 0, 0, InterpolationFlags.Linear);

// 使用双线性插值混合原始L通道和增强后的L通道
Mat blendedL = new Mat();
Cv2.AddWeighted(channels[0], 0.5, resizedL, 0.5, 0, blendedL);

// 用混合后的L通道替换原始L通道
channels[0] = blendedL;

// 合并通道
Cv2.Merge(channels, output);

// 转换回BGR颜色空间
Cv2.CvtColor(output, output, ColorConversionCodes.Lab2BGR);

return output;
}

在 ApplyCLAHE 方法中:

  1. 仍然将图像转换到 LAB 颜色空间并分离通道。
  2. 对 L 通道应用 CLAHE,但结果保存在一个新的 Mat enhancedL 中。
  3. 使用 Cv2.Resize 方法将增强后的 L 通道调整回原始大小,使用双线性插值(InterpolationFlags.Linear)。
  4. 使用 Cv2.AddWeighted 方法将原始 L 通道和增强后的 L 通道混合。这里使用了 0.5 的权重,您可以根据需要调整这个值。
  5. 用混合后的 L 通道替换原始 L 通道。
  6. 最后,合并通道并转换回 BGR 颜色空间。

这种方法应该能显著减少块状效果,使结果更加平滑自然。您可以调整以下参数来进一步优化结果:

  • CLAHE 的对比度限制(当前为 2.0)
  • CLAHE 的网格大小(当前为 8x8)
  • 混合时原始和增强 L 通道的权重(当前各为 0.5)

视频加载与摄像头调用

视频/摄像头加载

1
cv::VideoCapture::VideoCapture(const String& filename,int apiPreference=CAP_ANY)
  • filename: 读取的视频文件或图像序列名称
  • apiPreference: 读取数据时设置的属性,例如编码格式,是否调用OpenNI等

支持的摄像头类型

  1. USB 摄像头:这是最常见的摄像头类型,您可以通过 cv::VideoCapture 类直接访问 USB 摄像头。通常情况下,您可以使用设备索引(例如 01 等)来打开 USB 摄像头。

  2. 网络摄像头:OpenCV 也支持通过网络协议(如 RTSP 或 HTTP)访问网络摄像头。只需提供网络摄像头的 URL,您就可以使用 cv::VideoCapture 来打开和读取视频流。例如:

    1
    2
    3
    cv::VideoCapture cap("http://<ip_address>:<port>/video"); // HTTP 流
    // 或者
    cv::VideoCapture cap("rtsp://<ip_address>:<port>/stream"); // RTSP 流
  3. IP 摄像头:许多现代 IP 摄像头提供 RTSP 或 HTTP 流,您可以使用相同的方法通过 OpenCV 访问它们。

打开视频文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
VideoCapture video;
video.open("视频文件路径");
if(!video.isOpened())
{
cout<<"未成功打开"<<endl;
}
while(1)
{
Mat frame;
video>>frame;
if(frame.empty())
break;
imshow("video",frame);
uchar c = waitKey(1000/video.get(CAP_PROP_FPS));//等待每帧的时间,没输入就等"算出来的帧间隔"那么长时间
if(c=='q')//按q退出
{
break;
}
}

视频属性获取

通过get()函数可以获取下面的属性,如:cap.get(CAP_PROP_FRAME_WIDTH);

标志参数 简记 作用
CAP_PROP_POS_MSEC 0 视频文件的当前位置(以毫秒为单位)
CAP_PROP_FRAME_WIDTH 3 视频流中图像的宽度
CAP_PROP_FRAME_HEIGHT 4 视频流中图像的高度
CAP_PROP_FPS 5 视频流中图像的帧率(每秒帧数)
CAP_PROP_FOURCC 6 编解码器的4字符代码
CAP_PROP_FRAME_COUNT 7 视频流中图像的帧数
CAP_PROP_FORMAT 8 返回的Mat对象的格式
CAP_PROP_BRIGHTNESS 10 图像的亮度(仅适用于支持的相机)
CAP_PROP_CONTRAST 11 图像对比度(仅适用于相机)
CAP_PROP_SATURATION 12 图像饱和度(仅适用于相机)
CAP_PROP_HUE 13 图像的色调(仅适用于相机)
CAP_PROP_GAIN 14 图像的增益(仅适用于支持的相机)

如果使用的是opencv3以前的版本,属性名前还要加CV_

相关方法

  • bool isOpened() const: 检查视频是否成功打开。

  • bool read(OutputArray image): 读取视频中的一帧,并将其存储在Mat对象中。

  • void release(): 释放VideoCapture对象所占用的资源。

  • double get(int propId): 获取视频属性,如帧率、宽度、高度等。

  • bool set(int propId, double value): 设置视频属性,如帧率、宽度、高度等。

    调用 set 函数之后,后续捕获的帧或写入的视频将会应用新的属性设置。

视频图像逐帧获取

OpenCV为了减缓内存消耗,VideoWriter会将数据直接保存到外存,这样就可以保证在内存极小的情况下进行长时间录制,故嵌入式设备如果发现内存不足应将注意力放在其他地方,大部分时候都是Mat实例调用clone函数进行克隆,但未即使释放内存。

VideoWriter

1
cv::VideoWriter::VideoWriter(const String& filename,int fourcc,double fps,Size frameSize,bool isColor=true)
  • filename: 保存视频的地址和文件名,包含视频格式
  • fourcc: 压缩帧的4字符编解码器代码
  • fps: 保存视频的帧率,即视频中每秒图像的张数,这个数字若小于原视频帧率,则是快放,否则是慢放
  • frameSize: 视频帧的尺寸,是一个 cv::Size 对象,包含两个整数,分别表示视频帧的宽度和高度,这里的视频帧尺寸必须是和图像的大小一致,如果想要更改分辨率大小,必须将图像调整(使用 cv::resize 函数)之后再次进行保存才可以
  • isColor: 保存视频是否为彩色视频

例子: VideoWriter()

fourcc

用于在构造VideoWriter时设置编码格式,具体的设置需要有硬件提供支持,常见的有以下几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cv2.VideoWriter_fourcc('I','4','2','0') 
//这个选项是一个未压缩的YUV编码,4:2:0色度子采样。这种编码广泛兼容,但会产生大文件。文件扩展名应为.avi。

cv2.VideoWriter_fourcc('P','I','M','1')
//此选项为MPEG-1。文件扩展名应为.avi。


cv2.VideoWriter_fourcc('X','V','I','D')
//此选项是一个相对较旧的MPEG-4编码。如果要限制结果视频的大小,这是一个很好的选择。文件扩展名应为.avi。

cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
//此选项是另一个相对较旧的MPEG-4编码。如果要限制结果视频的大小,这是一个很好的选择。文件扩展名应为.m4v。

cv2.VideoWriter_fourcc('X','2','6','4'):
//这个选项是一种比较新的MPEG-4编码方式。如果你想限制结果视频的大小,这可能是最好的选择。文件扩展名应为.mp4。

cv2.VideoWriter_fourcc('H','2','6','4'):
//这个选项是传统的H264编码方式。如果你想限制结果视频的大小,这可能是很好的选择。文件扩展名应为.mp4。

cv2.VideoWriter_fourcc('T','H','E','O')
//这个选项是Ogg Vorbis。文件扩展名应为.ogv。

cv2.VideoWriter_fourcc('F','L','V','1')
//此选项为Flash视频。文件扩展名应为.flv。


cv2.VideoWriter_fourcc('M','J','P','G')
//此选项为motion-jpeg视频。文件扩展名应为.avi。

经验证:X264为压缩效率最高的编码格式,可以做到3s的视频802.6KB,H264次之为861.2KB,I420最大为131.3MB,在嵌入式开发板内建议使用X264

相关函数

isOpened函数

1
virtual bool isOpened() const;

Write函数

1
virtual void write(InputArray image);

该函数用于将一帧图片保存到文件里面

operator <<

1
VideoWriter& operator << (const Mat& image);

为了方便IO处理,也可用流控制符来完成写入操作,如writer << image;

析构函数

1
virtual void release();

实例技巧

优化帧率控制

  • 使用 Stopwatch 进行精确计时。

  • 计算每帧所需的时间间隔。

  • 不使用自旋等待来实现更精确的帧率控制

    在精细调整的自旋等待循环中,我们使用 Thread.Sleep(0) 代替了 Thread.SpinWait(1)

    这种方法可以在保持较好的帧率控制精度的同时,显著减少 CPU 负载。Thread.Sleep(0) 会让出当前线程的时间片,但不会导致线程进入长时间的睡眠状态,从而在降低 CPU 使用率和保持精确计时之间取得平衡。
    请注意,这种方法可能会略微降低帧率控制的精确度,但对于大多数应用来说,这种微小的差异是可以接受的,尤其是考虑到 CPU 使用率的显著降低。

  • 动态调整等待时间,以补偿处理时间的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/// <summary>
/// 刷新Frame帧
/// </summary>
private void RefreshFrame()
{
isCapture = true;
_lastSaveTime = DateTime.Now;
InitializeVideoWriter();

Task.Run(() =>
{
Stopwatch stopwatch = new Stopwatch();
long frameInterval = (long)(1000.0 / Fps * TimeSpan.TicksPerMillisecond);
long nextFrameTime = 0;

while (isCapture)
{
stopwatch.Start();
try
{
Mat tempFrame = new Mat();
if (!cap.Read(tempFrame))
{
Debug.WriteLine("无法读取帧");
continue;
}
if (tempFrame.Empty())
{
Debug.WriteLine("帧为空");
continue;
}

FrameChanged?.Invoke(tempFrame);
lock (_lockObject)
{
tempFrame.CopyTo(_BufferFrame);
}
if (isSave)
{
SaveFrame(tempFrame);
}

if (Application.Current == null)
break;
Application.Current.Dispatcher.Invoke(() =>
{
Frame = _BufferFrame.Clone();
});

long elapsedTicks = stopwatch.ElapsedTicks;
if (elapsedTicks < frameInterval)
{
long sleepTime = (frameInterval - elapsedTicks) / TimeSpan.TicksPerMillisecond;
if (sleepTime > 1)
Thread.Sleep((int)sleepTime - 1);
}

nextFrameTime += frameInterval;
while (stopwatch.ElapsedTicks < nextFrameTime)
{
Thread.Sleep(0);
}
}
catch (Exception ex)
{
Debug.WriteLine($"捕获帧时发生错误: {ex.Message}");
break;
}
}
CloseVideoWriter();
});
}

CSharp案例

摄像头视频显示并录制案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
VideoCapture _capture = new VideoCapture(0); // 打开默认摄像头
_capture.Set(VideoCaptureProperties.FrameWidth, 320); // 设置宽度
_capture.Set(VideoCaptureProperties.FrameHeight, 240); // 设置高度
Timer _timer = new Timer(40);
_timer.Elapsed += OnTimerTick; // 绑定计时器事件
string _outputFilePath = "output.mp4";
_timer.AutoReset = true; // 设置为自动重置
VideoWriter _videoWriter = new VideoWriter("output.mp4", FourCC.H264, 25, new OpenCvSharp.Size(320, 240), true);


private void OnTimerTick(object sender, EventArgs e)
{
using (var frame = new Mat()) // 创建一个Mat对象来存储帧
{
_capture.Read(frame); // 从摄像头读取一帧
if (!frame.Empty()) // 检查帧是否为空
{
// 将当前帧写入视频文件
_videoWriter.Write(frame);
// 使用异步方式更新UI,避免卡顿
Dispatcher.Invoke(() =>
{
// 显示原始视频帧
CameraImage.Source = BitmapSourceConverter.ToBitmapSource(frame);
});
}
}
}

// 窗口关闭事件处理
protected override void OnClosed(EventArgs e)
{
_timer.Stop(); // 停止计时器
_capture.Release(); // 释放摄像头资源
_videoWriter.Release(); // 释放视频写入对象
base.OnClosed(e); // 调用基类的OnClosed方法
}

这样保存出来的视频文件特别大

可以采取下面几种做法:

  • 采用H.265编码格式,获得更好的压缩率 FourCC.H264=> FourCC.HEVC
  • 智能码率控制:根据画面内容的复杂度和运动情况动态调整码率。画面简单、静止时降低码率,画面复杂、运动剧烈时适当提高码率,以在保证画质的同时减小文件大小。
  • 运动检测与区域编码:通过运动检测算法,区分静止区域和运动区域,对静止区域采用更高效的压缩方式,而对运动区域保证足够的编码精度。
  • 多分辨率编码:结合不同的分辨率进行编码,例如在画面静止时使用较低分辨率编码,运动时切换到较高分辨率。
  • 利用硬件编码加速:借助专门的硬件编码器(如 GPU 中的编码器)来加速编码过程,提高效率和压缩性能。

使用了Farneback光流算法来计算帧间的运动,并只更新变化的部分。这样可以减少每帧的更新量,提高视频流畅性

1

完整交互库案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
using OpenCvSharp;
using OpenCvSharp.Flann;
using OpenCvSharp.WpfExtensions;
using Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

namespace camera
{
public class CameraService : BindableBase
{
VideoCapture cap;
VideoWriter capWriter;
double FpsOrigin;
double WidthOrigin;
double HeightOrigin;
bool isCapture = false;//是否退出捕获线程
bool isSave = true;//是否保存

private Mat _BufferFrame = new();

private object _lockObject = new object();

private double _Fps;
public double Fps
{
get { return _Fps; }
set { SetProperty(ref _Fps, value); }
}

private double _Width;
public double Width
{
get { return _Width; }
set { SetProperty(ref _Width, value); }
}

private double _Height;
public double Height
{
get { return _Height; }
set { SetProperty(ref _Height, value); }
}

//帧对外开放读取
private Mat _Frame = new();
public Mat Frame
{
get { return _Frame; }
set { SetProperty(ref _Frame, value); }
}

private VideoWriter _videoWriter;
private DateTime _lastSaveTime;
private TimeSpan _saveInterval = TimeSpan.FromMinutes(1); // 默认5分钟保存一次
private string _saveDirectory = "SavedVideos"; // 保存视频的目录

public TimeSpan SaveInterval
{
get { return _saveInterval; }
set { SetProperty(ref _saveInterval, value); }
}

#region 委托与事件

public delegate void FrameChangedEventHandler(Mat frame);
public event FrameChangedEventHandler FrameChanged;

#endregion

public CameraService()
{
FrameChanged += AddOverlayTime;
}

public int getCameraNum()
{
return 1;
}

private void ShowVideoInfo()
{
FpsOrigin = cap.Get(VideoCaptureProperties.Fps);
WidthOrigin = cap.Get(VideoCaptureProperties.FrameWidth);
HeightOrigin = cap.Get(VideoCaptureProperties.FrameHeight);
Debug.WriteLine($"原始数据格式:{WidthOrigin}*{HeightOrigin} {FpsOrigin}Hz");
//估算码率
// long totalBytes = 0;
// totalBytes+= Frame.Total() * Frame.ElemSize();
// double bitrate = (totalBytes * 8) / elapsedSeconds; // 比特/秒
// Debug.WriteLine($"估算比特率: {bitrate / 1000000:F2} Mbps");
}

/// <summary>
/// 开始捕获摄像
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public bool StartCapture(int index = 0)
{
try
{
cap = new VideoCapture(index);
if (!cap.IsOpened())
return false;

//获取视频本身的各种数据
ShowVideoInfo();
Fps = FpsOrigin;
Width = WidthOrigin;
Height = HeightOrigin;
RefreshFrame();
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"启动摄像头时发生错误: {ex.Message}");
return false;
}
}

/// <summary>
/// 刷新Frame帧的线程,使用isCapture控制是否跳出循环,跳出循环时执行关闭写入并压缩覆盖
/// </summary>
private void RefreshFrame()
{
isCapture = true;
_lastSaveTime = DateTime.Now;
InitializeVideoWriter();

Task.Run(() =>
{
Stopwatch stopwatch = new Stopwatch();
long frameInterval = (long)(1000.0 / Fps * TimeSpan.TicksPerMillisecond);
long nextFrameTime = 0;

while (isCapture)
{
stopwatch.Start();
try
{
Mat tempFrame = new Mat();
if (!cap.Read(tempFrame))
{
Debug.WriteLine("无法读取帧");
continue;
}
if (tempFrame.Empty())
{
Debug.WriteLine("帧为空");
continue;
}

FrameChanged?.Invoke(tempFrame);
lock (_lockObject)
{
tempFrame.CopyTo(_BufferFrame);
}

if (isSave)
{
SaveFrame(tempFrame);
}

if (Application.Current == null)
break;
Application.Current.Dispatcher.Invoke(() =>
{
Frame = _BufferFrame.Clone();
});

long elapsedTicks = stopwatch.ElapsedTicks;
if (elapsedTicks < frameInterval)
{
long sleepTime = (frameInterval - elapsedTicks) / TimeSpan.TicksPerMillisecond;
if (sleepTime > 1)
Thread.Sleep((int)sleepTime - 1);
}

nextFrameTime += frameInterval;
while (stopwatch.ElapsedTicks < nextFrameTime)
{
Thread.Sleep(0);
}
}
catch (Exception ex)
{
Debug.WriteLine($"捕获帧时发生错误: {ex.Message}");
return;
}
}
//跳出循环的时候压缩视频文件
CloseVideoWriter();

});
}
string fileName = "";
string filePath = "";
private void InitializeVideoWriter()
{
if (!Directory.Exists(_saveDirectory))
{
Directory.CreateDirectory(_saveDirectory);
}

fileName = $"{DateTime.Now:yyyyMMdd_HHmmss}.mp4";
filePath = Path.Combine(_saveDirectory, fileName);

//_videoWriter = new VideoWriter(filePath, FourCC.MP4V, Fps, new OpenCvSharp.Size(Width, Height));
_videoWriter = new VideoWriter(filePath, FourCC.XVID, Fps, new OpenCvSharp.Size(Width, Height));
}

private void SaveFrame(Mat frame)
{
_videoWriter.Write(frame);

if (DateTime.Now - _lastSaveTime >= SaveInterval)
{
CloseVideoWriter();
InitializeVideoWriter();
_lastSaveTime = DateTime.Now;
}
}

string _ffmpegPath = @"D:\ffmpeg-7.0.2-full_build\bin\ffmpeg.exe";
// 启动FFmpeg进程
private void StartFFmpegProcess(string absolutePath)
{
string directory = Path.GetDirectoryName(absolutePath);
string fileName = Path.GetFileNameWithoutExtension(absolutePath);
string tempOutputPath = Path.Combine(directory, $"{fileName}_temp.mp4");

var arguments = $"-i \"{absolutePath}\" -c:v libx264 -preset slow -crf 26 -an \"{tempOutputPath}\"";

var processStartInfo = new ProcessStartInfo
{
FileName = _ffmpegPath,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = directory
};

using (Process process = new Process { StartInfo = processStartInfo })
{
process.Start();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();

if (process.ExitCode != 0)
{
Debug.WriteLine($"FFmpeg 执行失败。错误:{error}");
}
else
{
if (File.Exists(tempOutputPath))
{
File.Delete(absolutePath);
File.Move(tempOutputPath, absolutePath);
Debug.WriteLine("文件压缩并替换成功");
}
else
{
Debug.WriteLine("临时输出文件未找到");
}
}
}
}



private void CloseVideoWriter()
{
_videoWriter?.Release();
_videoWriter?.Dispose();
//压缩视频
string absolutePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filePath);
Task.Run(() => StartFFmpegProcess(absolutePath)).Wait();

}

public void ChangeCameraInfo()
{
isCapture = false;
// 设置分辨率
cap.Set(VideoCaptureProperties.FrameWidth, Width);
cap.Set(VideoCaptureProperties.FrameHeight, Height);

// 设置帧率
cap.Set(VideoCaptureProperties.Fps, Fps);
// 验证设置是否成功
double actualWidth = cap.Get(VideoCaptureProperties.FrameWidth);
double actualHeight = cap.Get(VideoCaptureProperties.FrameHeight);
double actualFps = cap.Get(VideoCaptureProperties.Fps);
Debug.WriteLine($"设置的分辨率: {Width}x{Height}, 实际分辨率: {actualWidth}x{actualHeight}");
Debug.WriteLine($"设置的帧率: {Fps}, 实际帧率: {actualFps}");
cap.Release();
cap = new VideoCapture(0);
if (!cap.IsOpened())
{
Debug.WriteLine("重新打开摄像头失败");
return;
}
RefreshFrame();
}

/// <summary>
/// 添加时间文字信息
/// </summary>
/// <param name="frame"></param>
private void AddOverlayTime(Mat frame)
{
String nowStr = DateTime.Now.ToString();
// 设置文本参数
double fontScale = 0.7;
int thickness = 2;
HersheyFonts font = HersheyFonts.HersheySimplex;

// 计算文本大小
OpenCvSharp.Size textSize = Cv2.GetTextSize(nowStr, font, fontScale, thickness, out int baseline);

// 计算文本位置(右上角,留出10像素的边距)
OpenCvSharp.Point textOrg = new OpenCvSharp.Point(frame.Width - textSize.Width - 10, textSize.Height + 10);

// 创建一个与原始帧相同大小和类型的覆盖层
Mat overlay = new Mat(frame.Size(), frame.Type());
overlay.SetTo(new Scalar(0, 0, 0, 0)); // 完全透明

// 绘制半透明背景
OpenCvSharp.Rect bgRect = new OpenCvSharp.Rect(textOrg.X - 5, textOrg.Y - textSize.Height - 5, textSize.Width + 10, textSize.Height + 10);
Cv2.Rectangle(overlay, bgRect, new Scalar(0, 0, 0, 0), -1);

// 将背景叠加到原始帧上
Cv2.AddWeighted(overlay, 0.5, frame, 1.0, 0, frame);

// 绘制透明背景文本(但必须在带有alpha的RGBA图像上)
// OpenCvSharp.Rect bgRect = new OpenCvSharp.Rect(textOrg.X - 5, textOrg.Y - textSize.Height - 5, textSize.Width + 10, textSize.Height + 10);
// Cv2.Rectangle(frame, bgRect, new Scalar(0, 0, 0, 0), -1);

// 绘制文本
Cv2.PutText(frame, nowStr, textOrg, font, fontScale, new Scalar(255, 255, 255), thickness, LineTypes.AntiAlias);
}

public void UpdateOverlayDate()
{

}

public void StopCapture()
{
isCapture = false;
cap.Release();
CloseVideoWriter();
////压缩视频
//string absolutePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filePath);
//Task.Run(() => StartFFmpegProcess(absolutePath));
}
}
}

音频库

C#音频库

MIT开源 开源地址

音频,视频录制后合并音视频案例

均匀分窗体算法

1 – 1

2 – 2

3 – 2

4 – 2

5 – 3

6 – 3

9 – 3

10 – 4