大家好哦,小编来讲程序啦,好好看哦,慢慢体会。。。一、说明实验介绍本次实验将使用利用 OpenCV 来实现对视频中感兴趣的动态物体的追踪。
首先我贴出我的水下追踪效果图吧
在图中我们可以看到鱼的追踪轨迹,以及鱼的个数
实验涉及的知识点
C++ 语言基础
g++ 的使用
图像基础
OpenCV 在图像及视频中的应用
Meanshift 和 Camshift 算法
二、环境搭建进行本节的实验时,您需要先完成了解一些linux 基本指令和图像处理知识才能进行下面的相关学习。
创建视频文件视频可以通过水下摄像头获取,当然了我们同样可以实时性处理,前面打开摄像头实验已经介绍过了,此实验只不过添加了meanshift和camshift.前面扯过的东西不知道大家看了没有,我再扯一遍吧,看看那图像的基本知识。。。。。
图像基础OpenCV 是一个开源的跨平台计算机视觉库,与 OpenGL 所提供的图像绘制不同,OpenCV 实现了图像处理和计算机视觉方面的很多通用的算法。在学习 OpenCV 之前,我们需要先了解一下图像、视频在计算机中的一些基本概念。
首先,我们要理解图片在计算机中的表示方式:图像在显示生活中以连续变化的形式而存在,但在计算机中,有两种常见的存储图片的方式:一种是矢量图,一种则是像素图。
矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。
而更常见的则是像素图,比如常说的一张图片的尺寸为1024*768,这就意味着这张图片水平方向上有1024个像素,垂直方向上有768个像素。
像素就是表示像素图的基本单位,通常情况下一个像素由三原色(红绿蓝)混合而成。由于计算机的本质是对数字的识别,一般情况下我们把一种原色按亮度的不同从 0~255 进行表示,换句话说,对于原色红来说,0表示最暗,呈现黑色,255表示最亮,呈现纯红色。
这样,一个像素就可以表示为一个三元组(B,G,R),比如白色可以表示为(255,255,255),黑色则为(0,0,0),这时我们也称这幅图像是 RGB 颜色空间中的一副图像,R、G、B 分别成为这幅图像的三个通道,除了 RGB 颜色空间外,还有很多其他的颜色空间,如 HSV、YCrCb 等等。
像素是表示像素图的基本单位,而图像则是表示视频的基本单位。一个视频由一系列图像组成,在视频中我们称其中的图像为帧。而通常我们所说的视频帧率,意味着这个视频每秒钟包含多少帧图像。比如帧率为 25,那么这个视频每秒钟就会播放25帧图像(人眼认为少于25帧差不多就不流畅了,所以一般你得设置为25以上)。
1秒钟共有1000毫秒,因此如果帧率为 rate 那么每一帧图像之间的时间间隔为 1000/rate。
图像颜色直方图颜色直方图是描述图像的一种工具,它和普通的直方图类似,只是颜色直方图需是根据某一幅图片计算而来。
如果一副图片是 RGB 颜色空间,那么我们可以统计 R 通道中,颜色值为 0~255 这 256中颜色出现的次数,这边能获得一个长度为 256 的数组(颜色概率查找表),我们再将所有的值同时除以这幅图像中像素的总数,将这之后所得的数列转化为直方图,其结果就是颜色直方图。
直方图反向投影人们已经证明,在 RGB 颜色空间中,对光照亮度变化较为敏感,为了减少此变化对跟踪效果的影响,就需要对直方图进行反向投影。这一共分为三个步骤:
首先将图像从RGB空间转换到HSV空间。
然后对其中的 H 颜色通道的直方图。
将图像中每个像素的值用颜色概率查找表中对应的概率进行替换,就得到了颜色概率分布图。
这个过程就叫反向投影,颜色概率分布图是一个灰度图像。
OpenCV 初步使用首先安装 OpenCV:
sudo apt-get install libopencv-dev(前面已经讲过了,这里再执行一遍,如果安装过会自动忽略)我们已经熟悉了 C++的基本语法,几乎在写过的每一个程序中都有使用到 #include <iostream> 和 using namespace std; 或者 std::cout。OpenCV 也有它自己的命名空间。
使用 OpenCV,只需要包含这一个头文件:
#include <opencv2/opencv.hpp> // OpenCV 头文件
int main() {
// 创建一个视频捕获对象
// OpenCV 提供了一个 VideoCapture 对象,它屏蔽了
// 从文件读取视频流和从摄像头读取摄像头的差异,当构造
// 函数参数为文件路径时,会从文件读取视频流;当构造函
// 数参数为设备编号时(第几个摄像头, 通常只有一个摄像
// 头时为0),会从摄像头处读取视频流。
cv::VideoCapture video("xiaorun.avi"); // 读取文件
// cv::VideoCapture video(0); // 使用摄像头
// 捕获画面的容器,OpenCV 中的 Mat 对象
// OpenCV 中最关键的 Mat 类,Mat 是 Matrix(矩阵)
// 的缩写,OpenCV 中延续了像素图的概念,用矩阵来描述
// 由像素构成的图像。
cv::Mat frame;
while(true) {
// 将 video 中的内容写入到 frame 中,
// 这里 >> 运算符是经过 OpenCV 重载的
video >> frame;
// 当没有帧可继续读取时,退出循环
if(frame.empty()) break;
// 显示当前帧
cv::imshow("test", frame);
// 录制视频帧率为 15, 等待 1000/15 保证视频播放流畅。
// waitKey(int delay) 是 OpenCV 提供的一个等待函数,
// 当运行到这个函数时会阻塞 delay 毫秒的时间来等待键盘输入
int key = cv::waitKey(1000/15);
// 当按键为 ESC 时,退出循环
if (key == 27) break;
}
// 释放申请的相关内存
cv::destroyAllWindows();
video.release();
return 0;
}
在shell里敲入 g++ main.cpp pkg-config opencv --libs --cflags opencv
-o main
运行 ./main 可以看到成功播放视频:
理解了 Camshift 算法的基本思想之后我们就可分析实现这个代码主要分为几个步骤了:
设置选择追踪目标的鼠标回调事件;
从视频流中读取图像;
实现 Camshift 过程;
下面我们继续修改 main.cpp 中的代码:
第一步:选择追踪目标区域的鼠标回调函数与 OpenGL 不同,在 OpenCV 中,对鼠标的回调函数指定了五个参数,其中前三个是我们最需要的:通过 event 的值我们可以获取这次回调鼠标发生的具体事件,如左键被按下(CV_EVENT_LBUTTONDOWN)、左键被抬起(CV_EVENT_LBUTTONUP) 等。
bool selectObject = false; // 用于标记是否有选取目标
int trackObject = 0; // 1 表示有追踪对象 0 表示无追踪对象 -1 表示追踪对象尚未计算 Camshift 所需的属性
cv::Rect selection; // 保存鼠标选择的区域
cv::Mat image; // 用于缓存读取到的视频帧
// OpenCV 对所注册的鼠标回调函数定义为:
// void onMouse(int event, int x, int y, int flag, void *param)
// 其中第四个参数 flag 为 event 下的附加状态,param 是用户传入的参数,我们都不需要使用
// 故不填写其参数名
void onMouse( int event, int x, int y, int, void* ) {
static cv:Point origin;
if(selectObject) {
// 确定鼠标选定区域的左上角坐标以及区域的长和宽
selection.x = MIN(x, origin.x);
selection.y = MIN(y, origin.y);
selection.width = std::abs(x - origin.x);
selection.height = std::abs(y - origin.y);
// & 运算符被 cv::Rect 重载
// 表示两个区域取交集, 主要目的是为了处理当鼠标在选择区域时移除画面外
selection &= cv::Rect(0, 0, image.cols, image.rows);
}
switch(event) {
// 处理鼠标左键被按下
case CV_EVENT_LBUTTONDOWN:
origin = cv:Point(x, y);
selection = cv::Rect(x, y, 0, 0);
selectObject = true;
break;
// 处理鼠标左键被抬起
case CV_EVENT_LBUTTONUP:
selectObject = false;
if( selection.width > 0 && selection.height > 0 )
trackObject = -1; // 追踪的目标还未计算 Camshift 所需要的属性
break;
}
}
第二步:从视频流中读取图像我们已经在之前实现了读取视频流的基本结构,下面我们进一步细化:
int main() {
cv::VideoCapture video("video.ogv");
cv::namedWindow("CamShift at Shiyanlou");
// 1. 注册鼠标事件的回调函数, 第三个参数是用户提供给回调函数的,也就是回调函数中最后的 param 参数
cv::setMouseCallback("CamShift at Shiyanlou", onMouse, NULL);
cv::Mat frame; // 接收来自 video 视频流中的图像帧
// 2. 从视频流中读取图像
while(true) {
video >> frame;
if(frame.empty()) break;
// 将frame 中的图像写入全局变量 image 作为进行 Camshift 的缓存
frame.copyTo(image);
// 如果正在选择追踪目标,则画出选择框
if( selectObject && selection.width > 0 && selection.height > 0 ) {
cv::Mat roi(image, selection);
bitwise_not(roi, roi); // 对选择的区域图像反色
}
imshow("CamShift at Shiyanlou", image);
int key = cv::waitKey(1000/15.0);
if(key == 27) break;
}
// 释放申请的相关内存
cv::destroyAllWindows();
video.release();
return 0;
}
提示
ROI(Region of Interest),在图像处理中,被处理的图像以方框、圆、椭圆、不规则多边形等方式勾勒出需要处理的区域,称为感兴趣区域,ROI。
第三步:实现 Camshift 过程计算追踪目标的反向投影直方图为需要先使用 cvtColor 函数,这个函数可以将 RGB 颜色空间的原始图像转换到 HSV 颜色空间。计算直方图必须在选择初始目标之后,因此:
int main() {
cv::VideoCapture video("video.ogv");
cv::namedWindow("CamShift at Shiyanlou");
cv::setMouseCallback("CamShift at Shiyanlou", onMouse, NULL);
cv::Mat frame;
cv::Mat hsv, hue, mask, hist, backproj;
cv::Rect trackWindow; // 追踪到的窗口
int hsize = 16; // 计算直方图所必备的内容
float hranges[] = {0,180}; // 计算直方图所必备的内容
const float* phranges = hranges; // 计算直方图所必备的内容
while(true) {
video >> frame;
if(frame.empty()) break;
frame.copyTo(image);
// 转换到 HSV 空间
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
// 当有目标时开始处理
if(trackObject) {
// 只处理像素值为H:0~180,S:30~256,V:10~256之间的部分,过滤掉其他的部分并复制给 mask
cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 10), mask);
// 下面三句将 hsv 图像中的 H 通道分离出来
int ch[] = {0, 0};
hue.create(hsv.size(), hsv.depth());
cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);
// 如果需要追踪的物体还没有进行属性提取,则对选择的目标中的图像属性提取
if( trackObject < 0 ) {
// 设置 H 通道和 mask 图像的 ROI
cv::Mat roi(hue, selection), maskroi(mask, selection);
// 计算 ROI所在区域的直方图
calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
// 将直方图归一
normalize(hist, hist, 0, 255, CV_MINMAX);
// 设置追踪的窗口
trackWindow = selection;
// 标记追踪的目标已经计算过直方图属性
trackObject = 1;
}
// 将直方图进行反向投影
calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
// 取公共部分
backproj &= mask;
// 调用 Camshift 算法的接口
cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
// 处理追踪面积过小的情况
if( trackWindow.area() <= 1 ) {
int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
trackWindow.x + r, trackWindow.y + r) & cv::Rect(0, 0, cols, rows);
}
// 绘制追踪区域
ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );
}
if( selectObject && selection.width > 0 && selection.height > 0 ) {
cv::Mat roi(image, selection);
bitwise_not(roi, roi);
}
imshow("CamShift at Shiyanlou", image);
int key = cv::waitKey(1000/15.0);
if(key == 27) break;
}
cv::destroyAllWindows();
video.release();
return 0;
}
四、总结本节实现的代码下面的代码实现仅供参考:
#include
bool selectObject = false;
int trackObject = 0;
cv::Rect selection;
cv::Mat image;
void onMouse( int event, int x, int y, int, void* ) {
static cv:Point origin;
if(selectObject) {
selection.x = MIN(x, origin.x);
selection.y = MIN(y, origin.y);
selection.width = std::abs(x - origin.x);
selection.height = std::abs(y - origin.y);
selection &= cv::Rect(0, 0, image.cols, image.rows);
}
switch(event) {
case CV_EVENT_LBUTTONDOWN:
origin = cv:Point(x, y);
selection = cv::Rect(x, y, 0, 0);
selectObject = true;
break;
case CV_EVENT_LBUTTONUP:
selectObject = false;
if( selection.width > 0 && selection.height > 0 )
trackObject = -1;
break;
}
}
int main( int argc, const char** argv )
{
cv::VideoCapture video(0);
cv::namedWindow( "CamShift at Shiyanlou" );
cv::setMouseCallback( "CamShift at Shiyanlou", onMouse, 0 );
cv::Mat frame, hsv, hue, mask, hist, backproj;
cv::Rect trackWindow;
int hsize = 16;
float hranges[] = {0,180};
const float* phranges = hranges;
while(true) {
video >> frame;
if( frame.empty() )
break;
frame.copyTo(image);
cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
if( trackObject ) {
cv::inRange(hsv, cv::Scalar(0, 30, 10), cv::Scalar(180, 256, 256), mask);
int ch[] = {0, 0};
hue.create(hsv.size(), hsv.depth());
cv::mixChannels(&hsv, 1, &hue, 1, ch, 1);
if( trackObject < 0 ) {
cv::Mat roi(hue, selection), maskroi(mask, selection);
calcHist(&roi, 1, 0, maskroi, hist, 1, &hsize, &phranges);
normalize(hist, hist, 0, 255, CV_MINMAX);
trackWindow = selection;
trackObject = 1;
}
calcBackProject(&hue, 1, 0, hist, backproj, &phranges);
backproj &= mask;
cv::RotatedRect trackBox = CamShift(backproj, trackWindow, cv::TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));
if( trackWindow.area() <= 1 ) {
int cols = backproj.cols, rows = backproj.rows, r = (MIN(cols, rows) + 5)/6;
trackWindow = cv::Rect(trackWindow.x - r, trackWindow.y - r,
trackWindow.x + r, trackWindow.y + r) &
cv::Rect(0, 0, cols, rows);
}
ellipse( image, trackBox, cv::Scalar(0,0,255), 3, CV_AA );
}
if( selectObject && selection.width > 0 && selection.height > 0 ) {
cv::Mat roi(image, selection);
bitwise_not(roi, roi);
}
imshow( "CamShift at Shiyanlou", image );
char c = (char)cv::waitKey(1000/15.0);
if( c == 27 )
break;
}
cv::destroyAllWindows();
video.release();
return 0;
}
五、结果重新编译 main.cpp:
g++ main.cpp `pkg-config opencv --libs --cflags opencv` -o main
运行:
./main
在视频中用鼠标拖拽选择一个要追踪的物体,即可看到追踪的效果大家仔细揣摩,不要心急,好好理解这算法的内涵。。。。