文章目录
前言
接文章OpenCV图像与视频分析笔记 — 图像部分,本文章是视频部分的笔记。包括了视频流的基本操作、角点检测、直方图反向投影、背景替换和光流分析等内容。
本文章根据上述视频部分的内容,详细解释和例举了OpenCV4
提供的常见10个API
是如何进行常见的视频流分析。
视频部分
1. 视频流的基本操作
- 视频流的读写操作
- 获取视频流基本信息
/*
视频流读操作函数原型1
explicit修饰的构造函数不允许隐式类型转换,只能使用构造函数,如VideoCapture capture = 0是非法的,如果没有explicit修饰,就是合法的
index: 表示打开的摄像头索引号,0表示打开默认的摄像头
apiPreference: 指定了首选的捕获API,CAP_ANY是一个宏,表示让OpenCV选择最适合的API。如果需要指定的API,则可以通过传递相应的宏来实现
*/
explicit VideoCapture(int index, int apiPreference = CAP_ANY);
/*
视频流读操作函数原型2
filename: 表示需要读取的视频流文件绝对路径
其余参数和上述类似
*/
explicit VideoCapture(const String& filename, int apiPreference = CAP_ANY);
/*
视频流写操作函数原型
filename:视频流写的文件绝对路径
fourcc:视频流编解码算法,four character code,四字符代码,一个标识符,比如写入MP4类型的文件,可以使用VideoWriter::fourcc('m', 'p', '4', 'v')
fps:视频流写入的帧数
frameSize:视频流写入的每一帧大小
isColor:默认true表示彩色帧,false表示灰度帧
*/
VideoWriter(const String& filename, int fourcc, double fps, Size frameSize, bool isColor = true);
/*
获取视频流基本信息,openCV中的VideoCapture对象提供了get函数,用来获取视频流相关的信息
*/
Videocapture capture2("D:\\Photo\\rheed_video\\1.mp4");
//获取视频流每一秒的帧数
int fps = capture2.get(CAP_PROP_FPS);
//FOURCC: four character code,四字符代码,表示视频压缩算法类型的一种
int type = capture2.get(CAP_PROP_FOURCC);
//获取视频流每一帧的宽度
int width = capture2.get(CAP_PROP_FRAME_WIDTH);
//获取视频流每一帧的高度
int height = capture2.get(CAP_PROP_FRAME_HEIGHT);
//获取视频流的总帧数
int count_of_frames = capture2.get(CAP_PROP_FRAME_COUNT);
2. 图像的色彩空间实操
该部分对图像主要的色彩空间进行介绍,然后通过HSV色彩空间颜色范围的过滤,实现纯色背景的替换。
BGR色彩空间:是图像处理中最常见的颜色表示模型,一个像素点使用三通道表示(Blue, Green, Red),每一个通道占8 bit(0~255),所以对于BGR色彩空间来说,通常每一个像素点大小都是24位。
HSV色彩空间:也是图像处理中一种常用的色彩表示模型,它将颜色分解为三个独立的部分,色调(Hue),饱和度(Saturation)和明度(Value),和BGR色彩空间一样,每一个像素点都是24 bit。
- 色调:取值范围0-180,色调反映了颜色的基本类型,红色对应的色调大约是0,绿色大约是60,蓝色大约是120。
- 饱和度:取值范围0-255,饱和度代表颜色鲜艳的取值范围,值越大,颜色越鲜艳,反之,颜色越接近灰度,0表示灰度。
- 明度:表示颜色的亮度或者暗度,与饱和度不同的是,0表示黑色,取值范围是0到255。
GRAY色彩空间,单通道灰度图像,也是图像任务处理的常用色彩空间,由于其单通道的原因,处理效率通常较高,取值范围0-255,0表示黑色,255表示白色。
除了上述三种常见的图像处理色彩空间,还有诸如Lab、YCbCr等色彩空间,这些色彩空间都是不常见的,用于特定设备上的色彩空间。
/*
这是一个图像色彩空间转换的Demo,程序中包括了如何利用HSV色彩空间的特点,通过inRange函数对色调(Hue)进行过滤,进行纯色背景的更换。
*/
/*
inRange()函数原型:用于创建一个二值掩码图(binary mask),该掩码图的作用是,指示输入图像中,哪些像素点的值落在指定的范围之内。通常用于颜色分割或阈值(自己设定Scalar值)处理,从而突出指定范围内的颜色或阈值像素点。
src:输入的源图像,可以是单通道或者是多通道图像
lowerb:像素点每一个通道的下界,Array类型或者Scalar类型
upperb:像素点每一天通道的上界,Array类型或者Scalar类型
dst:输出的目标图像,和src大小一致,一般是二值掩码图
*/
void inRange(InputArray src, InputArray lowerb,InputArray upperb, OutputArray dst);
/*
色彩空间转换使用例子,利用HSV色彩空间提取ROI区域
*/
void QuickDemo::ImageColorConvert() {
VideoCapture capture2("D:\\Photo\\images\\01.mp4");
if(!capture2.isOpened()){
cout << "Open failure..." << endl;
return;
}
Mat frame2, hsv, lab, mask, result;
while (true) {
int c = waitKey(100 / 2);
if (c == 27) { //ESC
break;
}
bool ret = capture2.read(frame2); //读取视频帧到Mat对象
if (!ret) { //读取失败,一般视频到了尾部
break;
}
GaussianBlur(frame2, frame2, Size(7, 7), 0);
cvtColor(frame2, hsv, COLOR_BGR2HSV);
cvtColor(frame2, lab, COLOR_BGR2Lab);
//在hsv图像中过滤掉绿色背景,找到HSV对应绿色背景的H
inRange(hsv, Scalar(25, 43, 46), Scalar(77, 255, 255), mask);
//取反获取ROI区域
bitwise_not(mask, mask);
//根据二值图提取的ROI区域,对frame2进行位与操作,mask限制与操作的范围是ROI区域
bitwise_and(frame2, frame2, result, mask);
imshow("frame2", frame2);
imshow("hsv", hsv);
imshow("lab", lab);
imshow("result", result);
}
capture2.release();
}
3. 直方图反向投影
直方图反向投影,主要作用是估计图像中某个像素点的值与预先计算好的直方图像素分布的相似度,它通常用于对象检测、颜色分割等任务。在知道直方图反向投影之前,我们首先得知道什么是直方图。
直方图,是一个统计学的概念,它可以体现出一段连续值的分布情况,比如0到99之间,有100个整数,其中,小于50的整数有50个,大于等于50的整数有50个。我们可以定义横坐标为0表示小于50的整数这一类,为1表示大于等于50的整数这一类。这样,我们就完成了直方图的定义。
图像直方图,就是通过对图像中所有的像素点进行统计,得到像素点通道取值的分布,比如对于HSV色彩空间,可以通过图像直方图,描述出色调(Hue)的统计分布情况。
/*
直方图反向投影函数原型
images:Mat对象数组,也是进行直方图反向投影的目标
nimages:Mat对象数组中的图像个数
channels:进行反向投影的通道数组首地址,比如我们是基于HSV色彩空间的前两个通道进行反向投影,那么channels的结构为int channels[] = { 0,1 }
hist:进行直方图反向投影所需的直方图对象(Mat类)
backProject:直方图反向投影输出的结果
ranges:图像进行直方图统计的通道范围
scale:输出反向投影的缩放因子,默认1即可,值越大,可以提高输出反向投影结果的对比度,使得匹配的区域更加明显
uniform:表明是否使用均值(均衡化)直方图,默认true即可
*/
void calcBackProject( const Mat* images, int nimages, const int* channels, InputArray hist, OutputArray backProject, const float** ranges, double scale = 1, bool uniform = true);
/*
直方图函数原型
images:Mat对象数组,也是进行直方图反向投影的目标
nimages:Mat对象数组中的图像个数
channels:进行反向投影的通道数组首地址,比如我们是基于HSV色彩空间的前两个通道进行反向投影,那么channels的结构为int channels[] = { 0,1 }
mask:通过掩码图限制直方图统计的ROI区域,默认Mat()即可
hist:输出的直方图统计结果,Mat类型
dims:直方图的维度,大部分情况2即可
histSize:直方图的大小,int类型的数组
ranges:图像进行直方图统计的通道范围
uniform:表明是否使用均值(均衡化)直方图,默认true即可
accumulate:表示新计算的直方图是否加在旧的直方图结果上,默认false即可
*/
void calcHist( const Mat* images, int nimages, const int* channels, InputArray mask, OutputArray hist, int dims, const int* histSize, const float** ranges, bool uniform = true, bool accumulate = false);
/*
直方图反向投影使用例子
1.确定需要进行直方图计算的匹配图像
2.计算匹配图像的直方图
3.基于已知图像进行直方图反向投影
4.输出反向投影的结果
*/
void QuickDemo::HistogramInvertProjection() {
Mat src = imread("D:\\Photo\\images\\hand.jpg", IMREAD_COLOR);
Mat model = imread("D:\\Photo\\images\\hand_section.png", IMREAD_COLOR);
Mat model_hsv, src_hsv;
cvtColor(model, model_hsv, COLOR_BGR2HSV);
cvtColor(src, src_hsv, COLOR_BGR2HSV);
int h_bins = 512, s_bins = 512; //直方图的大小
int histSize[] = { h_bins,s_bins };
int channels[] = { 0,1 }; //统计的通道个数
Mat roiHist; //存储直方图结果
float h_range[] = { 0,180 }; //需要统计的每一个通道取值范围
float s_range[] = { 0,255 };
const float* ranges[] = { h_range,s_range }; //取值范围数组
//获取直方图
calcHist(&model_hsv, 1, channels, Mat(), roiHist, 2, histSize, ranges, true, false);
//对直方图进行通道归一化,归一化的范围是[0,255]
normalize(roiHist, roiHist, 0, 255, NORM_MINMAX, -1,Mat());
Mat back_projection; //存储反向投影结果
calcBackProject(&src_hsv, 1, channels, roiHist, back_projection, ranges, 1.0);
imshow("model_hsv", model_hsv);
imshow("src_hsv", src_hsv);
imshow("roiHist", roiHist);
imshow("back projection", back_projection);
}
4. Harris角点检测
角点检测是计算机视觉中获取图像特征的一种方式,那么,在图像中,什么是角点呢?角点通常由两个边缘相交而产生(注意:角点是一片像素区域,而不是一个像素点),对于同一场景,即使视角发生变化,角点都具有稳定性,角点无论在梯度方向上,还是梯度幅值(梯度向量的模)上都有着较大的变化。OpenCV中,提供了多种角点检测的API,这里先介绍最简单的Harris角点检测。
/*
Harris角点检测函数原型
src:单通道图像:CV_8UC1或者CV_32FC1,通常我们传入灰度图像CV_8UC1
dst:存储角点检测的图像,图像类型为CV_32FC1
blockSize:角点检测的窗口大小,这个窗口的作用是计算角点响应,通过对角点检测窗口在图像上移动,通过每一个像素点的梯度矩阵,得出角点响应值
ksize:Sobel导数核的大小,通常设置为3,导数核的作用是计算角点里面每一个像素点的梯度,为计算角点响应值提供参数
k:Harris角点检测自由参数,通常取值为0到0.04之间,较小的值会导致更多的角点被检测出来(可能会包含噪声),较大的值会导致较少的角点被检测出来(但是结果会更加可靠)。
borderType:如何处理图像边界上的像素,默认缺省参数即可
*/
void cornerHarris( InputArray src, OutputArray dst, int blockSize,int ksize, double k,int borderType = BORDER_DEFAULT);
/*
Mat对象归一化函数原型
src:输入的图像
dst:存储和src大小一致的Mat对象
alpha:进行归一化的最小值边界
beta:进行归一化的最大值边界
norm_type:归一化的类型,如果设置了alpha和beta的值,那么常用的类型是NORM_MINMAX
dtype:depth type,默认负值,规定dst类型和src类型一致,若dtype大于0,则dst只有颜色通道和src一致,图像深度depth = CV_MAT_DEPTH(dtype)
mask:规定进行归一化感兴趣的区域,如果是图像的全部范围,Mat()即可
*/
void normalize( InputArray src, InputOutputArray dst, double alpha = 1, double beta = 0, nt norm_type = NORM_L2, int dtype = -1, InputArray mask = noArray());
/*
图像缩放,取绝对值和转换为无符号8位整数的函数原型
从OpenCV官方的函数解释,该函数对图像进行了三种操作
1. 缩放 + 偏置值
2. 在第一步的基础上,对所有像素值取绝对值
3. 在第二步的基础上,将所有的像素值转换位CV_8UC1类型
src:输入需要处理的图像
dst:存储处理后的图像
alpha:缩放因子,如果为默认值1,不进行缩放
beta:偏置值,如果为默认值0,不进行偏置
很明显,如果调用该API,最后两个参数使用默认值,则只会对图像的像素值取绝对值,然后转换为CV_8UC1
*/
void convertScaleAbs(InputArray src, OutputArray dst, double alpha = 1, double beta = 0);
/*
使用例子,一般的,对图像进行角点检测的步骤如下:
1. 获取灰度图像
2. 确定角点检测的大小(计算角点响应值 corner response value),Sobel导数核的大小(计算像素点灰度值变化的梯度)
3. 调用cornerHarris进行Harris角点检测
4. 角点检测得出来的Mat对象是CV_32FC1类型
5. 对4得到的Mat对象调用normalize(),进行归一化得到新的Mat对象
6. 对5得到的Mat对象调用convertScaleAbs(),进行比例因子缩放,加上相应的偏置值,最后取绝对值得到新的Mat对象,该Mat对象就能比较直观的显示角点了
7(Optional). 可以根据6得到的角点检测结果,绘制到原图中
*/
void QuickDemo::HarrisCornerDemo() {
Mat src = imread("D://Photo//images//abc.png");
Mat gray, binary;
cvtColor(src, gray, COLOR_BGR2GRAY);
Mat dst;
int block_size = 5;
int k_size = 3;
double k = 0.04;
cornerHarris(gray, dst, block_size, k_size, k); //进行角点检测之后,得到的目标图像是单通道的FLOAT32类型
Mat dst_norm = Mat::zeros(dst.size(), dst.type());
normalize(dst, dst_norm, 0, 255, NORM_MINMAX, -1, Mat());
convertScaleAbs(dst_norm, dst_norm); //对图像的每一个通道进行比例因子缩放,加上偏置值,最后取绝对值得到目标图像
//draw corners
for (int row = 0; row < src.rows; ++row) {
for (int col = 0; col < src.cols; ++col) {
//corner response value
int rsp = dst_norm.at<uchar>(row, col);
if (rsp > 100) { //阈值设置为100
//一般地,角点检测窗口的中心像素点会被看作是角点的代表像素点,Point输入的是图像坐标的值,图像遍历是按行列的,注意转换
circle(src, Point(col, row), 3, Scalar(0, 0, 255), 2);
}
}
}
imshow("src", src);
imshow("gray", gray);
}
5. Shi-Tomas角点检测
Shi-Tomas角点检测的原理和Harris角点检测一样,只是到最后计算角点响应值的方式不一样,Shi-Tomas角点检测角点响应值为矩阵 M M M的特征值较小的那个,即 R = m i n ( λ 1 , λ 2 ) R = min(\lambda_{1}, \lambda_{2}) R=min(λ1,λ2),最后,设置一个阈值,只有角点响应值 R R R大于阈值时,才认为该区域是角点。
/*
Shi-Tomas角点检测函数原型1,OpenCV提供了两种重载的API
image:输入需要进行角点检测的图像,8位或者32位的单通道图像
corners:输出检测到的角点(注意,这是一个点集合,但是角点是一个区域,这里的点指的是角点中的代表性点)
maxCorners:检测最大的角点个数
qualityLevel:角点质量的下限,这个值介于0到1之间,该参数决定了角点检测的阈值,具体的,thresh = qualityLevel * max_value,max_value为整个图像中最大的角点响应值
minDistance:检测角点之间的最小欧几里得距离
mask:掩码值,Mat类型,指定角点检测的图像区域,mask对应的非零像素区域是可以进行角点检测的区域,默认Mat()即可
blockSize:角点检测的窗口大小,默认3*3
useHarrisDetector:是否使用Harris角点检测,默认false即可
k:如果使用Harris角点检测,k为计算角点响应值所需的参数,介于0到0.04之间
从Harris角点检测的原理可以知道,在进行角点响应值计算的时候,需要用到像素点的梯度值,Shi-Tomas角点检测提供的第一个API自己实现了梯度计算这一步骤
*/
void goodFeaturesToTrack( InputArray image, OutputArray corners, int maxCorners, double qualityLevel, double minDistance, InputArray mask = noArray(), int blockSize = 3, bool useHarrisDetector = false, double k = 0.04);
/*
Shi-Tomas角点检测函数原型2,与第一个API不同的是,该API多了一个gradientSize参数,该参数决定了计算像素点梯度时,使用Sobel导数核的大小
*/
void goodFeaturesToTrack( InputArray image, OutputArray corners, int maxCorners, double qualityLevel, double minDistance, InputArray mask, int blockSize, int gradientSize, bool useHarrisDetector = false, double k = 0.04);
/*
Shi_Tomas角点检测
*/
void QuickDemo::ShiTomasCornerDemo() {
VideoCapture capture("D://Photo//images//bike.avi");
if (!capture.isOpened()) {
cout << "Open failure........" << endl;
return;
}
int fps = capture.get(CAP_PROP_FPS);
while (1) {
int c = waitKey(1000 / fps);
if (c == 48) { // 0键退出
break;
}
Mat src;
bool tmp = capture.read(src);
if (!tmp) { //读取视频最后一帧,退出
break;
}
Mat gray, dst;
dst = src.clone();
cvtColor(src, gray, COLOR_BGR2GRAY);
vector<Point2f> corners; //存储ShiTomas角点检测的结果
double quality_level = 0.3;
int blockSize = 3;
int gradientSize = 3;
goodFeaturesToTrack(gray, corners, 200, quality_level, 3, Mat(), blockSize, gradientSize, false);//ShiTomas角点检测接口
//与cornerHarris()不同的是,Shi-Tomas角点检测API直接返回角点的点集合数组
for (auto corner : corners) {
circle(dst, corner, 4, Scalar(0, 0, 255), 2, LINE_AA);
}
imshow("src", src);
imshow("dst", dst);
}
capture.release();
}
6. 利用Image Watch调试程序,进行视频帧分析
本部分主要是针对Visual Studio 2019插件Image Watch的使用,通过Image Watch,在程序的调试过程中,我们可以直观的看到Mat对象的变化情况,在Image Watch中,我们可以查看图像中任意一点的像素位置和像素的取值,为了解程序逻辑提供了很大的帮助。
void QuickDemo::VideoFrameColorAnalysisAndExtract() {
VideoCapture capture("D://Photo//images//balltest.mp4");
if (!capture.isOpened()) {
cout << "Open failure........" << endl;
return;
}
int fps = capture.get(CAP_PROP_FPS);
while (1) {
Mat src, hsv, dst, mask;
int c = waitKey(1000 / fps);
if (c == 27) { // ESC键退出
break;
}
bool tmp = capture.read(src);
if (!tmp) { //读取视频最后一帧,退出
cout << "提取视频帧失败" << endl;
break;
}
GaussianBlur(src, src, Size(5, 5), 0);
//去除纯色背景,提取出ROI区域,以二值图的形式表示
cvtColor(src, hsv, COLOR_BGR2HSV);
inRange(hsv, Scalar(26, 43, 46), Scalar(34, 255, 255), mask);
mask = ~mask;
Mat blueBG = Mat::zeros(src.size(), src.type());
blueBG = Scalar(255, 180, 0);
src.copyTo(blueBG, mask); //以mask的规则将src图片的内容复制到blueBG中去
//进行轮廓发现
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(mask, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point());
int index = -1;
double max_area = -1;
for (size_t i = 1; i < contours.size(); ++i) { //一般的,第一个轮廓存储的是整个图像的背景
if (contourArea(contours[i]) > max_area) {
index = i;
max_area = contourArea(contours[i]);
}
}
//轮廓拟合,跟踪目标物体
if (index >= 0) {
RotatedRect rrt = minAreaRect(contours[index]);
ellipse(src, rrt, Scalar(180, 160, 255), 2, LINE_AA);
circle(src, rrt.center, 2, Scalar(0, 0, 255), 2, LINE_AA);
}
imshow("src", src);
imshow("hsv", hsv);
imshow("mask", mask);
imshow("blueBG", blueBG);
}
capture.release();
}
7. 视频帧背景分析(通过背景减去器,提取前景的ROI区域)
在OpenCV中,提供了三种视频背景分析的方法,分别是:K最近邻算法(K Nearest Neighbors,KNN),高斯混合模型(Gaussian Mixture Model,GMM),模糊积分(Fuzzy Integral)。
- KNN算法的基本工作原理是,使用像素的历史值来估计背景模型。它通过维护一个历史帧队列,对于每个像素点,算法存储最近K帧内的值。当新的帧到来时,计算新帧像素点值与历史帧队列中K最近邻像素值的距离,如果距离大于某个阈值,则认为该像素属于前景,否则,属于背景。
- GMM算法的基本工作原理是,对于每一个像素,算法使用一个高斯混合模型估计背景分布,模型中包含几个高斯分布,每一个高斯分布代表一个不同的背景状态,高斯分布的参数(均值和方差)是根据像素历史值动态更新的。当新帧到来时,GMM算法会计算该像素与背景模型中各个高斯分布概率密度函数的似然,如果似然低于某个阈值,则认为该像素属于前景,否则,属于背景。
- 模糊积分是一种基于模糊逻辑的方法,它结合多个背景减除的结果来提高准确性,如使用KNN和GMM方法,生成多个背景模型。对于新帧的每一个像素,模糊积分算法会计算其在不同背景模型中的前景可能性,最后,使用模糊逻辑运算符来计算最终的前景掩码。
下面通过使用混合高斯模型(GMM)的背景减除器来实现背景的消除。
/*
基于混合高斯模型的背景减除器函数原型
history:该参数决定,算法学习历史帧的长度,较大的值可以提供更稳定和更准确的背景模型,但也会增加处理时间
varThreshold:该参数控制像素之间的方差阈值(历史帧),用于判断该像素点是否属于背景,如果一个像素方差低于阈值,则认为是背景,否则认为是前景,较高的阈值会检测更多的背景,减少误报,但可能会错过一些前景对象。不难理解,对于静态的背景,一般像素方差会较小,而对于移动的前景,像素方差会较大
detectShadows:是否尝试检测阴影部分,默认为true即可,有助于提高检测的准确性
return:返回值为BackgroundSubtractorMOG2类的智能指针类型,通过该智能指针,可以获取前景和背景的相关信息
*/
Ptr<BackgroundSubtractorMOG2> createBackgroundSubtractorMOG2(int history=500, double varThreshold=16, bool detectShadows=true);
/*
使用例子
*/
void QuickDemo::VideoBackgroundAnalysis() {
VideoCapture capture("D://Photo//images//opencv_demo//vtest.avi");
if (!capture.isOpened()) {
cout << "Open failure........" << endl;
return;
}
auto pMOG2 = createBackgroundSubtractorMOG2(1000, 200, true);
int fps = capture.get(CAP_PROP_FPS);
while (1) {
Mat src, mask, bg_image;
int c = waitKey(1000 / fps);
if (c == 27) { // ESC 键退出
break;
}
bool tmp = capture.read(src);
if (!tmp) { //读取视频最后一帧,退出
cout << "提取视频帧失败" << endl;
break;
}
GaussianBlur(src, src, Size(5, 5), 0);
//通过背景减去API返回的智能指针,获取前景和背景相关信息
pMOG2->apply(src, mask);
pMOG2->getBackgroundImage(bg_image);
//形态学开操作,先腐蚀,再膨胀
Mat kernel = getStructuringElement(MORPH_RECT,Size(1,5),Point(-1,-1));
morphologyEx(mask, mask, MORPH_OPEN, kernel, Point(-1, -1));
//基于掩码图所获得的轮廓信息,在原图中标记移动对象
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(mask, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point());
for (size_t i = 0; i < contours.size(); ++i) {
double area = contourArea(contours[i]);
if (area < 200) continue;
Rect rect = boundingRect(contours[i]);
//绘制矩形边框
rectangle(src, rect, Scalar(0, 200, 255), 2, LINE_AA);
//绘制椭圆边框
RotatedRect rrt = minAreaRect(contours[i]);
ellipse(src, rrt, Scalar(255, 200, 0), 2, LINE_AA); //最小外接矩形
circle(src, rrt.center, 2, Scalar(0, 0, 255), 2, LINE_AA);
}
imshow("src", src);
imshow("bg_image", bg_image);
imshow("mask", mask);
}
capture.release();
}
8. 基于光流法的视频分析 - 上
光流可以看成是图像结构光的变化,或者是图像亮度模式明显的移动。简单的理解就是,光流描述了连续两帧之间,像素点的位移向量。光流法是基于视频分析所提出的概念,OpenCV提供了两种光流分析方法,稀疏光流分析和稠密光流分析。
/*
基于稀疏光流法(KLT)的视频分析函数原型
该函数原型执行的是Lucas-Kanade光流算法,是一种经典的稀疏光流计算方法,用于追踪一组预选特征点(基于角点检测获取预选特征点),在连续图像帧中的运动。
preImg:8位单通道灰度图像,上一帧图像,光流计算的起点
nextImg:和preImg相同大小和类型的图像,当前帧图像,光流计算的目标帧
prePts:preImg中,通过角点检测出来的旧特征点
nextPts:nextImg中,通过API利用角点检测计算出来的新特征点
status:指示每一个特征点的追踪状态,如果特征点追踪成功,对应的status值为1,否则,status值为0,vector<uchar>类型
err:指示每一个特征点的追踪误差,vector<float>
winSize:用于计算光流的邻域窗口大小,更大的窗口可以提供更好的稳定性,但是可能牺牲精度,默认是Size(21, 21)
maxLevel:表示光流计算中,使用金字塔结构的最深层级,金字塔用于多尺度分析(因为Lucas-Kanade算法在假设像素点移动距离很小时才有效,当移动距离很大时,考虑将图像分辨率缩小,比如底层图像是原始分辨率100*100,倒数第二层图像是50*50的分辨率),可以提高追踪的鲁棒性,默认值为3
criteria:光流搜索迭代算法的终止条件,包括迭代次数和两次迭代间变化的最小阈值
flags:标志位,指定额外的行为选项,默认为0即可
minEigThreshshold:最小特征值阈值,用于确定光流矩阵的奇异性和稳定性,当像素点对应的光流矩阵,其最小特征值小于该阈值时,那么该像素点的光流估计会被认为是不可靠的(无效的特征点),反之,该像素点光流估计可靠。但是,并不是minEigThreshold设置的越小越好,太小的话,会导致本就不可靠的特征点被误判为可靠,默认为1e-4即可。
*/
void calcOpticalFlowPyrLK( InputArray prevImg, InputArray nextImg, InputArray prevPts, InputOutputArray nextPts, OutputArray status, OutputArray err, Size winSize = Size(21,21), int maxLevel = 3, TermCriteria criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01), int flags = 0, double minEigThreshold = 1e-4);
/*
基于稀疏光流法(KLT)的视频分析使用例子
*/
RNG rng(1234);
void DrawLines(Mat& frame, const vector<Point2f>& pts1, const vector<Point2f>& pts2) {
for (size_t i = 0; i < pts1.size(); ++i) {
line(frame, pts1[i], pts2[i], Scalar(rng.uniform(0,256), rng.uniform(0, 256), rng.uniform(0, 256)), 2, 8, 0);
}
}
void QuickDemo::VideoOpticalFlowAnalysis1() {
VideoCapture capture("D:\\Photo\\images\\balltest.mp4");
if (!capture.isOpened()) {
cout << "Open video failure...." << endl;
return;
}
namedWindow("frame", WINDOW_AUTOSIZE);
//对视频第一帧进行角点检测
Mat old_frame, old_gray;
capture.read(old_frame);
cvtColor(old_frame, old_gray, COLOR_BGR2GRAY);
vector<Point2f> initPoints;
vector<Point2f> feature_pts;
double quality_level = 0.01;
int minDistance = 10;
//shi-Tomas角点检测
goodFeaturesToTrack(old_gray, feature_pts, 100, quality_level, minDistance, Mat(), 3, false);
Mat frame, gray;
vector<Point2f> pts[2]; //定义了存储两个vector<Point2f>的数组,存储新帧和旧帧的特征点集合
pts[0].insert(pts[0].end(), feature_pts.begin(), feature_pts.end()); //旧帧特征点集合
initPoints.insert(initPoints.end(), feature_pts.begin(), feature_pts.end());
vector<uchar> status;
vector<float> error;
TermCriteria criteria = TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 10, 0.01); //停止标准
while (true) {
bool isGrabbed = capture.read(frame);
if (!isGrabbed) break;
int c = waitKey(1000 / capture.get(CAP_PROP_FPS));
if (c == 27) break;
cvtColor(frame, gray, COLOR_BGR2GRAY);
//calculate optical flow, based on gray images, new frame's cotners will be calculated.
calcOpticalFlowPyrLK(old_gray, gray, pts[0], pts[1], status, error, Size(31, 31), 3, criteria, 0);
//遍历计算光流得出来的新帧点集合
int i=0, k = 0;
for (i = 0; i < pts[1].size(); ++i) {
//距离与状态检测
double dist = static_cast<double>(abs(pts[1][i].x - pts[0][i].x)) + static_cast<double>(abs(pts[1][i].y - pts[0][i].y));
if (dist > 2.0 && status[i]) {
//更新点集合状态,将status无效的点覆盖
pts[0][k] = pts[0][i];
pts[1][k] = pts[1][i];
initPoints[k] = initPoints[i];
++k;
int b = rng.uniform(0, 256);
int g = rng.uniform(0, 256);
int r = rng.uniform(0, 256);
circle(frame, pts[1][i], 2, Scalar(b, g, r), 2, 8); //在新帧中绘制特征点
line(frame, pts[0][i], pts[1][i], Scalar(b, g, r), 2, 8, 0); //新帧中绘制光流变化直线
}
}
//update key points
pts[0].resize(k); //只有前k个点的status有效,pts[0]随着循环的进行,会不断缩小
pts[1].resize(k);
initPoints.resize(k);
//基于初始点绘制跟踪线
DrawLines(frame, initPoints, pts[1]);
//update to old
std::swap(old_gray, gray);
cv::swap(pts[0], pts[1]);
//re-init
if (pts[0].size() < 40) {
//shi-Tomas角点检测
goodFeaturesToTrack(old_gray, feature_pts, 200, quality_level, minDistance, Mat(), 3, false);
pts[0].insert(pts[0].end(), feature_pts.begin(), feature_pts.end());
initPoints.insert(initPoints.end(), feature_pts.begin(), feature_pts.end());
}
imshow("KLT_Demo", frame);
}
capture.release();
}
9. 基于光流法的视频分析 - 下
稠密光流分析法,与稀疏光流分析法不同的是,稠密光流算法估计每一像素点的光流向量(运动矢量),这意味着整个图像中的所有像素都会被考虑,从而产生一个稠密的位移向量场。OpenCV中,稠密光流分析法是基于$ Gunnar Farneback$算法。
/*
稠密光流视频分析法函数原型
prev:单通道8位灰度图像,光流计算的起点
next:和prev同大小、同类型的图像,光流计算的目标帧
flow:存储每个像素点的光流向量,Mat<Point2f>类型
pyr_scale:金字塔缩放比例,用于创建多尺度金字塔,例如,0.5表示下一层金字塔图像尺寸是上一层的一半,默认值0.5即可
levels:金字塔的深度,更大的深度意味着更加精细的尺度分析,默认值3即可
winsize:计算光流时使用邻域的窗口大小,窗口越大,光流场越平滑,但是可能会丢失细节,默认值15即可
iterations:在每一个金字塔层中迭代优化光流的次数,更多的迭代次数可以提高光流的精度,默认值3即可
poly_n:用于进行多项式扩展的邻域大小,在计算光流时,会对每个像素点的邻域进行多项式拟合,以提高估计的准确性,默认值5即可
poly_sigma:多项式拟合时的高斯权重函数的标准差,它决定了邻域内哪些点对中心点的影响更大默认值1.1即可
flags:用于指定附加行为的标志位,默认值0即可
*/
void calcOpticalFlowFarneback(InputArray prev, InputArray next, InputOutputArray flow, double pyr_scale, int levels, int winsize, int iterations, int poly_n, double poly_sigma, int flags);
/*
将笛卡尔系的二维坐标向量转换为极坐标系
x:输入的x坐标,通常是单通道的Mat类对象,CV_32F或CV_64F
y:输入的y坐标,类型和x的类型一致
magnitude:存储极坐标系下向量的幅度(向量的模)
angle:存储极坐标系下向量的角度,即与x轴正向的夹角
angleDegrees:角度的单位,默认false即弧度制,true即角度制
*/
void cartToPolar(InputArray x, InputArray y, OutputArray magnitude, OutputArray angle, bool angleInDegrees = false);
/*
稠密光流分析法使用例子,基本步骤
1. 对视频流第一帧图像进行灰度化,作为第一个旧帧
2. 在循环中,提取第一个新帧,灰度化
3. 调用稠密光流分析法函数
4. 提取光流向量在x轴和y轴方向上的偏移量
5. 将偏移量转换为极坐标表示
6. 将转换为极坐标的角度映射到0~180,极坐标向量的幅度映射到0~255(利用归一化),然后将角度的映射作为H,设置mv[1]为S,幅度的映射作为V,进行通道合并成HSV图像
7. 最后,将HSV图像转换为BGR图像,将稠密光流分析的结果展示出来
*/
void QuickDemo::VideoOpticalFlowAnalysis2() {
VideoCapture capture("D://Photo//images//opencv_demo//vtest.avi");
if (!capture.isOpened()) {
cout << "Open failure........" << endl;
return;
}
Mat frame, pre_frame;
Mat gray, pre_gray;
capture.read(pre_frame);
cvtColor(pre_frame, pre_gray, COLOR_BGR2GRAY);
Mat hsv = Mat::zeros(pre_frame.size(), pre_frame.type());
Mat mag = Mat::zeros(pre_frame.size(), CV_32FC1); //32位单通道浮点数
Mat ang = Mat::zeros(pre_frame.size(), CV_32FC1);
Mat xpts = Mat::zeros(pre_frame.size(), CV_32FC1);
Mat ypts = Mat::zeros(pre_frame.size(), CV_32FC1);
vector<Mat> mv;
split(hsv, mv); //通道分离
Mat result;
double fps = capture.get(CAP_PROP_FPS);
while (1) {
int c = waitKey(1000.0 / fps);
if (c == 27) break;
bool tmp = capture.read(frame);
if (!tmp) { //读取视频最后一帧,退出
break;
}
cvtColor(frame, gray, COLOR_BGR2GRAY);
Mat flow; //稠密光流分析法输出检测的像素点,也就是每个像素点的光流向量
calcOpticalFlowFarneback(pre_gray, gray, flow, 0.5, 3, 15, 3, 5, 1.2, 0);
//提取光流向量的x和y轴偏移量
for (int row = 0; row < flow.rows; ++row) {
for (int col = 0; col < flow.cols; ++col) {
const Point2f& flow_xy = flow.at<Point2f>(row, col);
xpts.at<float>(row, col) = flow_xy.x;
ypts.at<float>(row, col) = flow_xy.y;
}
}
//将光流向量转换为极坐标表示
cartToPolar(xpts, ypts, mag, ang);
//向量的角度映射到0到180度表示
ang = ang * 180 / CV_PI / 2;
//将向量的模归一化,映射到0到255
normalize(mag, mag, 0, 255, NORM_MINMAX);
//对极坐标系下向量的幅度和角度取绝对值,转换为CV_8UC1类型
convertScaleAbs(mag, mag);
convertScaleAbs(ang, ang);
//将类型转换后的ang和mag进行通道合并,hsv色彩空间
mv[0] = ang;
mv[1] = Scalar(255);
mv[2] = mag;
merge(mv, hsv);
cvtColor(hsv, result, COLOR_HSV2BGR);
imshow("frame", frame);
imshow("result", result);
}
capture.release();
}
10. 均值迁移分析法
/*
均值迁移分析法函数原型
*/
/*
均值迁移分析法使用例子
*/
void QuickDemo::MeanValueShiftAnalysis() {
VideoCapture capture("D://Photo//images//balltest.mp4");
if (!capture.isOpened()) {
cout << "Open failure........" << endl;
return;
}
double fps = capture.get(CAP_PROP_FPS);
Mat frame, hsv, hue, mask, hist, backproj;
capture.read(frame);
bool init = true;
Rect trackWindow;
int hsize = 16;
float hranges[] = { 0,180 };
const float* ranges[] = { hranges };
//从frame中提取ROI区域
Rect selection = selectROI("MeanShift Demo", frame, true, false);
while (1) {
int c = waitKey(1000.0 / fps);
if (c == 27) break;
bool isCrabbed = capture.read(frame);
if (!isCrabbed) break;
cvtColor(frame, hsv, COLOR_BGR2HSV);
inRange(hsv, Scalar(26, 43, 46), Scalar(34, 255, 255), mask);
int ch[] = { 0,0 };
hue.create(hsv.size(), hsv.depth());
mixChannels(&hsv, 1, &hue, 1, ch, 1); //通道混合
//初始化
if (init) {
Mat roi(hue, selection);
Mat mask_roi(mask, selection);
//计算直方图
calcHist(&roi, 1, 0, mask_roi, hist, 1, &hsize, ranges);
normalize(hist, hist, 0, 255, NORM_MINMAX);
trackWindow = selection;
init = false;
}
//计算直方图反向投影
calcBackProject(&hue, 1, 0, hist, backproj, ranges);
//直方图反向投影的结果与掩码图进行与操作
backproj &= mask;
//迭代次数和两次迭代间变化的最小阈值
TermCriteria criteria = TermCriteria(TermCriteria::COUNT | TermCriteria::EPS, 30, 1);
meanShift(backproj, trackWindow, criteria);
//均值迁移分析
RotatedRect rrt = CamShift(backproj, trackWindow, criteria);
//绘制矩形
rectangle(frame, trackWindow, Scalar(0, 0, 255), 2, LINE_AA);
//绘制椭圆
ellipse(frame, rrt, Scalar(255, 200, 0), 2, LINE_AA);
imshow("MeanShift Demo", frame);
}
capture.release();
}
总结
至此,OpenCV
图像与视频分析相关的笔记整理完毕,该系列笔记包含了OpenCV
大部分传统的方法,来进行图像处理,非常适合OpenCV
的入门。