技术 C++ opencv ZEROKO14 2024-04-29 2024-10-19 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显示器等大部分都是采用这种模型。自然界中的任何一种颜色都可以由红、绿、蓝三种色光混合而成,现实生活中人们见到的颜色大多是混合而成的色彩。
组合方法是通过互补光的形式来组合成任意颜色的
三通道想要组合成黑白灰,必须三原色值相同 ,当某一方的值不相同时就会产生其他颜色。
灰度图不一定是单通道,但是单通道一定是灰度图
将RGB图像转变为灰度图像遵循下面的公式:(仅需了解) $$ gray = B0.114+G 0.587+R*0.299 $$
HSV颜色空间
RGB颜色空间的分量与亮度密切相关,即只要改变亮度,3个分量都会随之相应地改变.因此,RGB颜色空间适合于显示系统,却并不适合于图像处理
色调: 表示观察者感知的主要颜色
饱和度: 指的是相对的纯净度,或一种颜色混合白光的数量
如深红色(红加白)和淡紫色(紫加白)这样的色彩是欠饱和的,饱和度与所加的白光的数量成反比
亮度: 表达了无色的强度概念
因为,色调+饱和度 = 色度,所以: $$ 颜色 = 色调+饱和度+亮度 = 色度+亮度 $$
图像的基本类型 在计算机中,我们常见的图像类型有以下几种:
二值化图像 : 一幅二值图像的二维矩阵仅由0、1两个值构成,“0”代表黑色,“1”代白色。由于每一像素(矩阵中每一元素)取值仅有0、1两种可能,所以计算机中二值图像的数据类型通常为1个二进制位。二值图像通常用于文字、线条图的扫描识别(OCR)和掩膜图像的存储。
灰度图像: 灰度图像矩阵元素的取值范围通常为[0,255]。因此其数据类型一般为8位无符号整数的(int8),这就是人们经常提到的256灰度图像。“0”表示纯黑色,“255”表示纯白色,中间的数字从小到大表示由黑到白的过渡色。在某些软件中,灰度图像也可以用双精度数据类型(double)表示,像素的值域为[0,1],0代表黑色,1代表白色,0到1之间的小数表示不同的灰度等级。二值图像可以看成是灰度图像的一个特例。
**索引图像:**索引图像的文件结构比较复杂,除了存放图像的二维矩阵外,还包括一个称之为颜色索引矩阵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真彩色图像。
**真彩色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轴的正方向;
VGA = 640 x 480
HD = 1280 ×720
FHD = 1920 x 1080
4K = 3840 ×2160
灰度图像:从0~256($2^8$)
对于彩色图像,有三个灰度图像,分别代表红色,绿色和蓝色的强度.将他们合在一起就是完整的彩色图像
opencv环境配置 mac配置 brew install opencv 或vcpkg 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 ); }
读取摄像头并窗口显示
1 2 3 4 5 6 7 VideoCapture cap (0 ) ;Mat img; while (true ){ cap.read (img); imshow ("Image" ,img); waitKey (1 ); }
Mat是opencv引入的矩阵类型(Matrix)
Mat类 OpenCV官方参考文档
结构构成
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数据在内存中存放形式
常用属性
属性
作用
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 ;
两个矩阵相乘
矩阵乘积 * a = b*c;
向量内积 .dot a.dot(b)
对应位元素乘积 .mul() a.mul(b)
[[数学#乘法意义盘点|上述各种乘法的现实意义参考此处]]
相关函数
函数名
作用
absdiff()
两个矩阵对应元素差的绝对值
add()
两个矩阵求和
addWeighted()
两个矩阵线性求和
divide()
矩阵除法
invert()
矩阵求逆
log()
矩阵求对数
max()/min()
两个矩阵计算最大值/最小值.min(a, b) 会返回一个新的矩阵,其中每个元素都是 a 和 b 中对应位置元素的最小值
矩阵的线性求和: 是指将多个矩阵按一定的权重进行求和。具体来说,线性求和涉及到对每个矩阵的元素乘以一个标量(权重),然后再进行求和。
矩阵求和: 是指将两个相同维度的矩阵对应位置的元素相加,得到一个新的矩阵。
矩阵除法: 实际上是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);Mat imgBlur; GaussianBlur (img,imgBlur,Size (3 ,3 ),3 ,0 );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 ();
亮度与对比度 ConvertTo 和 ConvertScaleAbs 是 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; resize (img,imgResize,Size (400 ,200 ));resize (img,imgResize2,Size (),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 ();
绘制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Mat img (512 ,512 ,CV_8UC3,Scalar(255 ,255 ,255 )) ;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 ();
仿射变换
仿射变换是线性变换和平移的组合。
仿射变换保持直线的性质,但不保持原点不变。
仿射变换可以用矩阵乘法和向量加法来表示
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); 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 ();
颜色检测 直方图均衡化 直方图均衡化(Histogram Equalization) 是一种用于图像处理的技术,目的是增强图像的对比度,使得图像的亮度分布更加均匀。这种方法尤其适合提升灰度图像的对比度,但也可以扩展到彩色图像。
像的直方图 表示图像中不同灰度级(亮度)的像素分布情况。例如,假设一个灰度图像的像素值在 0(黑色)到 255(白色)之间,那么直方图就展示了每个灰度级出现的次数。通过分析直方图,可以判断图像是否过亮、过暗,或对比度过低。
直方图均衡化 的核心思想是通过重新分布图像的灰度值,使得像素在所有灰度级上的分布更为均匀,从而增加图像的整体对比度。
工作原理
通过累积分布函数重新分配图像的灰度级,从而均衡亮度并提升细节
计算直方图:首先,计算图像中每个灰度值的像素数量,得到图像的直方图。
计算累积分布函数 (CDF):累积分布函数表示某个灰度级及其以下的所有像素值在图像中所占的总比例。计算每个灰度值的累积概率分布。公式如下: $$ CDF(i) = \sum_{j=0}^{i} p(j) $$ 其中,p(j) 是第 j 灰度值出现的概率。
灰度值映射:根据累积分布函数,将每个灰度值映射到一个新的灰度值区间。新的灰度值会是原始值的线性扩展,从而平衡亮度分布。公式为: $$ new_pixel_value = CDF(i) \times (L - 1) $$ 其中,L 是灰度级数(例如,对于 8 位图像,L 为 256)。
生成均衡化后的图像:将所有像素点的灰度值通过映射转换为新的值,得到对比度增强的图像。
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 假设有一个简单的直方图,灰度值为 0 到 3 ,频率如下: | 灰度值 | 频率 | |--------|------| | 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 cv2import numpy as npimg = 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_img = cv2.cvtColor(img, cv2.COLOR_BGR2YUV) yuv_img[:, :, 0 ] = cv2.equalizeHist(yuv_img[:, :, 0 ]) 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 的一种改进,它通过限制对比度增强的强度,防止过度增强。
原理
分块处理 :将图像分成多个小块(通常称为“网格”或“区块”),并对每个小块分别进行直方图均衡化。每个小块的对比度都根据其局部直方图来调整。
插值 :为了避免块与块之间出现明显的边界(块效应),会对相邻块的均衡化结果进行[[算法#插值算法|双线性插值]],使图像变得更平滑。
对比度限制 :在 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 ; } cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE (2.0 , cv::Size (8 , 8 )); cv::Mat dst; 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 private Mat ApplyCLAHE (Mat input ){ Mat output = new Mat(); Cv2.CvtColor(input, output, ColorConversionCodes.BGR2Lab); Mat[] channels = Cv2.Split(output); var clahe = Cv2.CreateCLAHE(2.0 , new OpenCvSharp.Size(8 , 8 )); Mat enhancedL = new Mat(); clahe.Apply(channels[0 ], enhancedL); Mat resizedL = new Mat(); Cv2.Resize(enhancedL, resizedL, channels[0 ].Size(), 0 , 0 , InterpolationFlags.Linear); Mat blendedL = new Mat(); Cv2.AddWeighted(channels[0 ], 0.5 , resizedL, 0.5 , 0 , blendedL); channels[0 ] = blendedL; Cv2.Merge(channels, output); Cv2.CvtColor(output, output, ColorConversionCodes.Lab2BGR); return output; }
在 ApplyCLAHE 方法中:
仍然将图像转换到 LAB 颜色空间并分离通道。
对 L 通道应用 CLAHE,但结果保存在一个新的 Mat enhancedL 中。
使用 Cv2.Resize 方法将增强后的 L 通道调整回原始大小,使用双线性插值(InterpolationFlags.Linear)。
使用 Cv2.AddWeighted 方法将原始 L 通道和增强后的 L 通道混合。这里使用了 0.5 的权重,您可以根据需要调整这个值。
用混合后的 L 通道替换原始 L 通道。
最后,合并通道并转换回 BGR 颜色空间。
这种方法应该能显著减少块状效果,使结果更加平滑自然。您可以调整以下参数来进一步优化结果:
CLAHE 的对比度限制(当前为 2.0)
CLAHE 的网格大小(当前为 8x8)
混合时原始和增强 L 通道的权重(当前各为 0.5)
视频加载与摄像头调用 视频/摄像头加载 1 cv::VideoCapture::VideoCapture (const String& filename,int apiPreference=CAP_ANY)
filename: 读取的视频文件或图像序列名称
apiPreference: 读取数据时设置的属性,例如编码格式,是否调用OpenNI等
支持的摄像头类型
USB 摄像头 :这是最常见的摄像头类型,您可以通过 cv::VideoCapture 类直接访问 USB 摄像头。通常情况下,您可以使用设备索引(例如 0、1 等)来打开 USB 摄像头。
网络摄像头 :OpenCV 也支持通过网络协议(如 RTSP 或 HTTP)访问网络摄像头。只需提供网络摄像头的 URL,您就可以使用 cv::VideoCapture 来打开和读取视频流。例如:
1 2 3 cv::VideoCapture cap ("http://<ip_address>:<port>/video" ) ; cv::VideoCapture cap ("rtsp://<ip_address>:<port>/stream" ) ;
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' ) { 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' ) cv2. VideoWriter_fourcc ('P' ,'I' ,'M' ,'1' ) cv2. VideoWriter_fourcc ('X' ,'V' ,'I' ,'D' ) cv2. VideoWriter_fourcc ('m' , 'p' , '4' , 'v' ) cv2. VideoWriter_fourcc ('X' ,'2' ,'6' ,'4' ): cv2. VideoWriter_fourcc ('H' ,'2' ,'6' ,'4' ): cv2. VideoWriter_fourcc ('T' ,'H' ,'E' ,'O' ) cv2. VideoWriter_fourcc ('F' ,'L' ,'V' ,'1' ) cv2. VideoWriter_fourcc ('M' ,'J' ,'P' ,'G' )
经验证: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 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 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()) { _capture.Read(frame); if (!frame.Empty()) { _videoWriter.Write(frame); Dispatcher.Invoke(() => { CameraImage.Source = BitmapSourceConverter.ToBitmapSource(frame); }); } } } protected override void OnClosed (EventArgs e ) { _timer.Stop(); _capture.Release(); _videoWriter.Release(); base .OnClosed(e); }
这样保存出来的视频文件特别大
可以采取下面几种做法:
采用H.265编码格式,获得更好的压缩率 FourCC.H264=> FourCC.HEVC
智能码率控制:根据画面内容的复杂度和运动情况动态调整码率。画面简单、静止时降低码率,画面复杂、运动剧烈时适当提高码率,以在保证画质的同时减小文件大小。
运动检测与区域编码:通过运动检测算法,区分静止区域和运动区域,对静止区域采用更高效的压缩方式,而对运动区域保证足够的编码精度。
多分辨率编码:结合不同的分辨率进行编码,例如在画面静止时使用较低分辨率编码,运动时切换到较高分辨率。
利用硬件编码加速:借助专门的硬件编码器(如 GPU 中的编码器)来加速编码过程,提高效率和压缩性能。
使用了Farneback光流算法来计算帧间的运动,并只更新变化的部分。这样可以减少每帧的更新量,提高视频流畅性
完整交互库案例
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 ); 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" ); } 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 ; } } 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.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" ; 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(); } 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); 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); 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(); } } }
音频库 C#音频库 MIT开源 开源地址
音频,视频录制后合并音视频案例
均匀分窗体算法 1 – 1
2 – 2
3 – 2
4 – 2
5 – 3
6 – 3
9 – 3
10 – 4