一.基本概念
光流的概念是Gibson于1950年提出的。所谓光流是指图像中模式运动的速度,光流场是一种二维(2D)瞬时速度场,其中二维速度向量是可见的三维速度向量在成像平面上的投影。光流法是把检测区域的图像变为速度的矢量场,每一个向量表示了景物中一个点在图像中位置的瞬时变化。因此,光流场携带了有关物体运动和景物三维结构的丰富信息,通过对速度场(光流场)的分析可以判断在检测区域内车辆的有无。
思路:求得整个图像检测区域的速度场,根据每个像素点的速度向量特征,可以对图像进行分析。如果是静止背景即无车通过时,则光流向量在整个检测区域是连续变化的;当有车通过时,光流向量必然和其邻域背景的光流向量不同,从而检测出车辆及其出现的位置。
光流法的前提假设:
(1)相邻帧之间的亮度恒定值,
(2)相邻视频帧的取帧的时间连续,或者相邻帧之间物体的运动比较“微小”;
(3)保持空间一致性,即,同一子图像的像素点具有相同的运动。
原理:
(1)对一个连续的视频帧序列进行处理;
(2)针对每一个视频序列,利用一定的目标检测方法,检测可能出现的前景目标;
(3)如果某一帧出现了前景目标,找到其具有代表性的关键特征点(如shi-Tomasi算法);
(4)对之后的任意两个相邻视频帧而言,寻找上一帧中出现的关键特征点在当前帧中的最佳位置,从而得到前景目标在当前帧中的位置坐标;
(5)如此迭代进行,便可实现目标的跟踪;
二.程序中重要函数解释
(1)void cvGoodFeaturesToTrack( const CvArr* image, CvArr* eig_image, CvArr* temp_image,CvPoint2D32f* corners, int* corner_count,double quality_level, double min_distance,const CvArr* mask=NULL );
程序中此函数代码:cvGoodFeaturesToTrack(frame1_1C, eig_image, temp_image, frame1_features, &number_of_features, .01, .01, NULL);
这是shi-Tomasi算法,该算法主要用于feature selection,即一张图中哪些是我 们感兴趣需要跟踪的点(interest point)
input:
* "frame1_1C" 输入图像.
* "eig_image" and "temp_image" 只是给该算法提供可操作的内存区域
* 第一个".01" 规定了特征值的最小质量,因为该算法要得到好的特征点,哪就需要一个选择的阈值
* 第二个".01" 规定了像素之间最小的距离,用于减少运算复杂度,当然也一定程度降低了跟踪精度
* "NULL" 意味着处理整张图片,当然你也可以指定一块区域
output:
* "frame1_features" 将会包含fram1的特征值
* "number_of_features" 将在该函数中自动填充上所找到特征值的真实数目
(2)迭代算法的终止准则
typedef struct CvTermCriteria
{
int type; /* CV_TERMCRIT_ITER 和CV_TERMCRIT_EPS二值之一,或者二者的组合 */
int max_iter; /* 最大迭代次数 */
double epsilon; /* 结果的精确性 */
}
(3) void cvCalcOpticalFlowPyrLK( const CvArr* prev, const CvArr* curr, CvArr* prev_pyr,CvArr* curr_pyr, const CvPoint2D32f* prev_features, CvPoint2D32f*curr_features, int count, CvSize win_size, int level, char* status, float*track_error, CvTermCriteria criteria, int flags );
计算一个稀疏特征集的光流,使用金字塔中的迭代 Lucas-Kanade 方法。参数:*prev在时间 t 的第一帧;*curr在时间 t + dt 的第二帧;*prev_pyr第一帧的金字塔缓存. 如果指针非 NULL , 则缓存必须有足够的空间来存储金字塔从层 1 到层 #level 的内容。尺寸 (image_width+8)*image_height/3 比特足够了;*curr_pyr与 prev_pyr 类似, 用于第二帧;*prev_features需要发现光流的点集;*curr_features包含新计算出来的位置的 点集;*count特征点的数目;*win_size每个金字塔层的搜索窗口尺寸;*level最大的金字塔层数。如果为 0 , 不使用金字塔 (即金字塔为单层), 如果为 1 , 使用两层,下面依次类推;*status数组。如果对应特征的光流被发现,数组中的每一个元素都被设置为 1,否则设置为 0;*error双精度数组,包含原始图像碎片与移动点之间的差。为可选参数,可以是 NULL ;*criteria准则,指定在每个金字塔层,为某点寻找光流的迭代过程的终止条件;*flags其它选项:(1)CV_LKFLOW_PYR_A_READY , 在调用之前,第一帧的金字塔已经准备好(2)CV_LKFLOW_PYR_B_READY , 在调用之前,第二帧的金字塔已经准备好(3)CV_LKFLOW_INITIAL_GUESSES , 在调用之前,数组 B 包含特征的初始坐标 (Hunnish: 在本节中没有出现数组 B,不知是指的哪一个)
函数 cvCalcOpticalFlowPyrLK 实现了金字塔中 Lucas-Kanade 光流计算的稀疏迭代版本([Bouguet00])。它根据给出的前一帧特征点坐标计算当前视频帧上的特征点坐标。函数寻找具有子象素精度的坐标值。
两个参数 prev_pyr 和 curr_pyr 都遵循下列规则:如果图像指针为 0, 函数在内部为其分配缓存空间,计算金字塔,然后再处理过后释放缓存。否则,函数计算金字塔且存储它到缓存中,除非设置标识 CV_LKFLOW_PYR_A[B]_READY 。 图像应该足够大以便能够容纳Gaussian 金字塔数据。调用函数以后,金字塔被计算而且相应图像的标识可以被设置,为下一次调用准备就绪 (比如:对除了第一个图像的所有图像序列,标识 CV_LKFLOW_PYR_A_READY 被设置).
三.程序源代码#include
#include
#include
#include
static const double pi = 3.14159265358979323846;
inline static double square(int a)
{
return a * a;
}
/*此函数主要目的:给img分配内存空间(除非此图像已经非NULL),并设定图像大小、位深以及通道数。
即使该图像的大小、深度或信道数与要求的不同,也会创建一个非NULL图像。*/
inline static void allocateOnDemand( IplImage **img, CvSize size, int depth, int channels)
{
if ( *img != NULL ) return;
*img = cvCreateImage( size, depth, channels );
if ( *img == NULL )
{
fprintf(stderr, "Error: Couldn't allocate image. Out of memory?\n");
exit(-1);
}
}
int main(void)
{
CvCapture *input_video = cvCaptureFromFile("video1.avi"); //创建一个对象,读取avi视频。
if (input_video == NULL)
{
fprintf(stderr, "Error: Can't open video.\n"); //视频不存在或格式不支持时显示
return -1;
}
cvQueryFrame( input_video ); // 读取一帧是为了获得帧的长宽。
CvSize frame_size;
frame_size.height = (int) cvGetCaptureProperty( input_video, CV_CAP_PROP_FRAME_HEIGHT );
frame_size.width = (int) cvGetCaptureProperty( input_video, CV_CAP_PROP_FRAME_WIDTH );
long number_of_frames; //视频帧的长度
cvSetCaptureProperty( input_video, CV_CAP_PROP_POS_AVI_RATIO, 1. ); //跳到视频结束,以便获取视频长度(帧数)
number_of_frames = (int) cvGetCaptureProperty( input_video, CV_CAP_PROP_POS_FRAMES ); //得到帧数
cvSetCaptureProperty( input_video, CV_CAP_PROP_POS_FRAMES, 0. ); //重新回到视频开始,以便下续步骤进行
/*开始进行光流法*/
cvNamedWindow("Optical Flow", CV_WINDOW_AUTOSIZE); //创建窗口,显示输出,大小自动调节
long current_frame = 0;
while(true)
{
static IplImage *frame = NULL, *frame1 = NULL, *frame1_1C = NULL, *frame2_1C =
NULL, *eig_image = NULL, *temp_image = NULL, *pyramid1 = NULL, *pyramid2 = NULL;
cvSetCaptureProperty( input_video, CV_CAP_PROP_POS_FRAMES, current_frame );
frame = cvQueryFrame( input_video ); //读取第一帧
if (frame == NULL)
{
fprintf(stderr, "Error: Hmm. The end came sooner than we thought.\n");
return -1;
}
allocateOnDemand( &frame1_1C, frame_size, IPL_DEPTH_8U, 1 ); //分配给frame1_1C内存空间
cvConvertImage(frame, frame1_1C, CV_CVTIMG_FLIP); //将帧数据赋给frame1_1C
allocateOnDemand( &frame1, frame_size, IPL_DEPTH_8U, 3 ); //把具有全部颜色信息的原帧保存,以备最后在屏幕上显示用
cvConvertImage(frame, frame1, CV_CVTIMG_FLIP);
frame = cvQueryFrame( input_video ); //读取第二帧
if (frame == NULL)
{
fprintf(stderr, "Error: Hmm. The end came sooner than we thought.\n");
return -1;
}
allocateOnDemand( &frame2_1C, frame_size, IPL_DEPTH_8U, 1 );
cvConvertImage(frame, frame2_1C, CV_CVTIMG_FLIP);
/* 施和托马斯特征跟踪! */
allocateOnDemand( &eig_image, frame_size, IPL_DEPTH_32F, 1 ); //分配需要的内存
allocateOnDemand( &temp_image, frame_size, IPL_DEPTH_32F, 1 ); //分配需要的内存
CvPoint2D32f frame1_features[400]; //创建数组,存放一帧的特征
int number_of_features; //函数运行前先设定特征数的最大值,运行后将是找到的特征数真正数量
number_of_features = 400; //可以改变此值,折中精确性
cvGoodFeaturesToTrack(frame1_1C, eig_image, temp_image, frame1_features, &number_of_features, .01, .01, NULL); //施和托马斯算法
/* 金字塔的 Lucas Kanade 光流法! */
CvPoint2D32f frame2_features[400]; //数组用于存放帧二中来自帧一的特征。
char optical_flow_found_feature[400]; //当且仅当frame1中的特征值在frame2中找到时,对应值为非零。
float optical_flow_feature_error[400]; //数组第i个元素表对应点光流误差
CvSize optical_flow_window = cvSize(3,3); //lucas-kanade光流法运算窗口,可以去其他大小,不过运算量加大
CvTermCriteria optical_flow_termination_criteria = cvTermCriteria( CV_TERMCRIT_ITER | CV_TERMCRIT_EPS, 20, .3 ); //终止条件
allocateOnDemand( &pyramid1, frame_size, IPL_DEPTH_8U, 1 );
allocateOnDemand( &pyramid2, frame_size, IPL_DEPTH_8U, 1 );
//跑Lucas Kanade算法
cvCalcOpticalFlowPyrLK(frame1_1C, frame2_1C, pyramid1, pyramid2, frame1_features,
frame2_features, number_of_features, optical_flow_window, 5,
optical_flow_found_feature, optical_flow_feature_error,
optical_flow_termination_criteria, 0 );
/* 画光流场,画图是依据两帧对应的特征值,这个特征值就是图像上我们感兴趣的点,如边缘上的点P(x,y)*/
for(int i = 0; i < number_of_features; i++)
{
if ( optical_flow_found_feature[i] == 0 ) continue; //如果Lucas Kanade算法没找到特征点,跳过
int line_thickness;
line_thickness = 1;
CvScalar line_color;
line_color = CV_RGB(255,0,0);
/* 画箭头,因为帧间的运动很小,所以需要缩放,不然看不见箭头,缩放因子为3 */
CvPoint p,q;
p.x = (int) frame1_features[i].x;
p.y = (int) frame1_features[i].y;
q.x = (int) frame2_features[i].x;
q.y = (int) frame2_features[i].y;
double angle;
angle = atan2( (double) p.y - q.y, (double) p.x - q.x ); //方向
double hypotenuse;
hypotenuse = sqrt( square(p.y - q.y) + square(p.x - q.x) ); //长度
q.x = (int) (p.x - 3 * hypotenuse * cos(angle)); //放大三倍
q.y = (int) (p.y - 3 * hypotenuse * sin(angle));
cvLine( frame1, p, q, line_color, line_thickness, CV_AA, 0 ); //画箭头主体
//画箭的头部
p.x = (int) (q.x + 9 * cos(angle + pi / 4));
p.y = (int) (q.y + 9 * sin(angle + pi / 4));
cvLine( frame1, p, q, line_color, line_thickness, CV_AA, 0 );
p.x = (int) (q.x + 9 * cos(angle - pi / 4));
p.y = (int) (q.y + 9 * sin(angle - pi / 4));
cvLine( frame1, p, q, line_color, line_thickness, CV_AA, 0 );
}
cvShowImage("Optical Flow", frame1);
int key_pressed;
key_pressed = cvWaitKey(33); //隔33ms显示一帧
/* 触发B键,视频回一帧 */
if (key_pressed == 'b' || key_pressed == 'B') current_frame--;
else current_frame++;
/* 不要超出视频的头和尾 */
if (current_frame < 0) current_frame = 0;
if (current_frame >= number_of_frames - 1) current_frame = number_of_frames - 2;
}
}
【整合】