VisionWorks快速入门--Immediate mode
VisionWorks快速入门(Immediate mode)
本教程演示如何通过用visionworks函数直接替换opencv函数,轻松地在计算机视觉应用程序中利用visionworks库。
好处是应用程序的性能得到了显著的提高(x2.5),而没有明显的代码复杂性。
准备工作
需要安装以下软件
- CMake2.8.12
- opencv2.4+
- VisionWorks 1.4+
基本问题和方法说明
我们使用视频去抖作为说明。视频去抖输入是一个抖动的视频,输出是一个稳定之后的视频。
视频稳定一个有点经典的算法是用来完成这项任务的。下面是一个简短的描述。
在执行过程中,算法保持了一个包含N个最新帧的队列
Q
f
Q_f
Qf。每两个图像之间的Homography矩阵组成另一个队列Ah,长度为
2
∗
(
N
−
1
)
2 *(N-1)
2∗(N−1).
下面说明一下步骤:
- 初始化 Q f Q_f Qf,包含0个frame,Ah中包含单位矩阵,i=0
- i= i+1,采集一张图片加入到 Q f Q_f Qf中,如果 Q f Q_f Qf中达到N了就删除最先压入的图像。
- 使用cv::Fast()从i-1帧中找特征点
- 使用特征点使用cv::calcOpticalFlowPyrLK()在当前帧上计算光流。
- 两帧图像中特征点计算出一个单应矩阵H(i)
- Push H(i)到Ah队列中,如果满了删除最早的
- 如果 i<4,这时图像还不够多,重复1,否则,通过多个单应矩相乘转换关系矩阵
T
n
T_n
Tn,计算(i-3)帧和当前帧的变换关系。
T1=H(i−5)⋅H(i−4)⋅H(i−3),
T2=H(i−4)⋅H(i−3) ,
T3=H(i−3),
T4=E,
T5=H−1(i−2),
T6=(H(i−2)⋅H(i−1))−1,
T7=(H(i−2)⋅H(i−1)⋅H(i))−1
使用1维高斯过滤矩阵 T n T_n Tn,使用 HomographySmoother::getSmoothedHomography()函数,
T = ∑ w n T n T=∑w_nT_n T=∑wnTn (n =1到7) , 其中 w i w_i wi是沿高斯曲线选择的高斯权重,使得帧最接近当前帧并且对结果矩阵的影响最大。 - 基于计算的矩阵T(cv::warpPerspective())将透视变换应用于第(i-3)帧。
- 在屏幕上显示第(i-3)帧并返回步骤1。
这就是整个视频去抖的原理,主要在于特征匹配然后计算位移,再进行高斯滤波。
下图展示了代码的流水线操作流程,蓝色的表示数据,黑色的表示执行算法:
运行瓶颈分析
为了使从opencv到visionworks的转换更容易,我们在所谓的即时模式中使用这些函数。这意味着每个函数都会立即执行,结果马上就可以得到,类似于普通的opencv函数。另一种选择是所谓的图形模式,将在下一个教程中讨论。
让我们看看opencv程序的性能。
FastCorners : 1.053 ms
ColorConvert : 0.198 ms
BuildPyramid : 0.770 ms
OpticalFlow : 11.354 ms
FindHomography : 1.563 ms
WarpPerspective : 2.286 ms
TOTAL : 17.368 ms
这些每帧结果是在ntel i5-6500 CPU和GeForce GTX 650 GPU电脑上获得的。
显然, cv::calcOpticalFlowPyrLK()函数占用了大部分时间,因此它是我们优化的第一个目标,即用VisionWorks替代品替换它。为了简化数据转换,我们还替换了两个相关函数:cv::buildOpticalFlowPyramid() and cv::calcOpticalFlowPyrLK().
迁移步骤
包含6步:
- 包含VisionWorks库
- 添加VisionWorks类数据代码,并初始化它们
- 将函数的输入数据从opencv数据类型转换为visionworks数据类型。
- 使用相应的visionworks函数。
- 将函数输出数据从visionworks数据类型转换为opencv类型。
- 释放申请的objects。
以下各节详细描述了这些步骤。
1. 包含VisionWorks库
CMakeLists.txt添加添加三行代码
- find_package(VisionWorks)
- include_directories("${VisionWorks_INCLUDE_DIRS}")
- target_link_libraries(vstab_demo ${VisionWorks_LIBRARIES})
库文件 头文件,然后添加上链接目标。然后 就可以在程序中调用 VisionWorks函数了。
类似地,我们也需要添加 NVXIO库,这个库包含工具和IO函数。例如,
NVXIO_SAFE_CALL用于处理VisionWorks函数返回的错误代码。
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)
PROJECT(vstab_demo)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
find_package(OpenCV REQUIRED core imgproc video highgui calib3d)
find_package(VisionWorks REQUIRED)
find_package(VisionWorks-NVXIO)
include_directories("${OpenCV_INCLUDE_DIRS}")
include_directories("${VisionWorks_INCLUDE_DIRS}")
include_directories("${VisionWorks-NVXIO_INCLUDE_DIRS}")
add_executable( vstab_demo main.cpp
homography_smoother.cpp homography_smoother.hpp
stabilizer.cpp stabilizer.hpp
)
target_link_libraries(vstab_demo
${OpenCV_LIBS}
${VisionWorks_LIBRARIES}
${VisionWorks-NVXIO_LIBRARIES}
)
2.添加VisionWorks类数据代码,并初始化它们
必要的函数
vxuGaussianPyramid(vx_context context,
vx_image input,
vx_pyramid gaussian
)
vxuOpticalFlowPyrLK(vx_context context,
vx_pyramid old_images,
vx_pyramid new_images,
vx_array old_points,
vx_array new_points_estimates,
vx_array new_points,
vx_enum termination,
vx_scalar epsilon,
vx_scalar num_iterations,
vx_scalar use_initial_estimate,
vx_size window_dimension
)
OpenCVStabilizer类中创建以下字段:
vx_context context_;
vx_image gray_frame_;
vx_pyramid latest_pyr_;
vx_pyramid current_pyr_;
vx_array vx_points_;
vx_array vx_corresponding_points_;
注意
,vx_pyramid latest_pyr_ 和 vx_pyramid current_pyr_用于替换程序opencv版本中的相应字段,即std::vectorcv::Mat latest_pyr_ and std::vectorcv::Mat current_pyr_.
这些字段初始化在OpenCVStabilizer::init()中
context_ = vxCreateContext();
vx_image gray = nvx_cv::createVXImageFromCVMat(context_, gray_latest_frame_);
NVXIO_CHECK_REFERENCE(gray);
vx_size width = gray_latest_frame_.cols;
vx_size height = gray_latest_frame_.rows;
latest_pyr_ = vxCreatePyramid(context_,
(vx_size)params_.num_pyramid_levels,
VX_SCALE_PYRAMID_HALF,
width,
height,
VX_DF_IMAGE_U8);
NVXIO_CHECK_REFERENCE(latest_pyr_);
NVXIO_SAFE_CALL(vxuGaussianPyramid(context_, gray, latest_pyr_));
current_pyr_ = vxCreatePyramid(context_,
(vx_size)params_.num_pyramid_levels,
VX_SCALE_PYRAMID_HALF,
width,
height,
VX_DF_IMAGE_U8);
vxReleaseImage(&gray);
vx_size capacity = 15000;
vx_enum item_type = NVX_TYPE_KEYPOINTF;
vx_points_ = vxCreateArray(context_, item_type, capacity);
NVXIO_CHECK_REFERENCE(vx_points_);
vx_corresponding_points_ = vxCreateArray(context_, item_type, capacity);
NVXIO_CHECK_REFERENCE(vx_corresponding_points_);
3.将函数的输入数据从opencv数据类型转换为visionworks数据类型。
数据格式转换已经在《VisionWorks学习之 如何和Opencv交互使用》中有专门的介绍,但在这里,我们将简要介绍一些重要的要点,例如opencv和visionworks数据类型之间的差异、使用visionworks类型的特殊性、用户很容易产生的错误/假设。
众所周知,opencv库中的基本数据类型是cv:Mat。从名称上看,它用于矩阵处理。同一类型用于存储“普通”矩阵和图像(cv::Mat可以有多个通道来表示图像平面)。VisionWorks有单独的数据类型用于这些目的:vx_image和vx_matrix。有关cv::Mat或cv::Gpu::GpuMat与vx_image或vx_matrix之间的转换的信息,请参见:《VisionWorks学习之 如何和Opencv交互使用》
我们的这个例子中相应的输入参数对应如下表
OpenCV | VisionWorks |
---|---|
cv::Mat gray_current_frame_ | vx_image input |
std::vector<cv::KeyPoint> key_points_ | vx_array old_points |
float opt_flow_epsilon | vx_scalar epsilon |
int opt_flow_num_iterations | vx_scalar num_iterations |
int opt_flow_use_initial_estimate | vx_scalar use_initial_estimate |
int opt_flow_win_size | vx_size window_dimension |
我们已经提到了如何将cv::Mat转换为vx_image,所以让我们考虑数组的转换。由于cv::KeyPoint和nvx::nvx_keypointf_t不兼容,我们需要在循环中转换元素。注意, cv::calcOpticalFlowPyrLK()使用std::vector<cv::Point2f>, vxuOpticalFlowPyrLK()使用VX_TYPE_KEYPOINT。与opencv函数不同,对vxuOpticalFlowPyrLK()来说,输入数组点的tracking_status非常重要。因为我们有所有有效点,所以将状态设置为1。
int points_count = key_points_.size();
std::vector<nvx_keypointf_t> vx_keypoint;
vx_keypoint.resize(points_count);
for (int i = 0; i < points_count; i++)
{
vx_keypoint[i].x = key_points_[i].pt.x;
vx_keypoint[i].y = key_points_[i].pt.y;
vx_keypoint[i].tracking_status = 1;
}
std::vector转化为vx_array还是比较方便的,因为他们有相同的数据类型。
vxTruncateArray(vx_points_, 0);
NVXIO_SAFE_CALL(vxAddArrayItems(vx_points_, (vx_size)points_count, (void *)&vx_keypoint[0], sizeof(vx_keypoint[0])));
其余的就是参数的转换了,float和int 转化为vx_scalar, int 转化为vx_size.
vx_scalar s_opt_flow_epsilon = vxCreateScalar(context_, VX_TYPE_FLOAT32, ¶ms_.opt_flow_epsilon);
NVXIO_CHECK_REFERENCE(s_opt_flow_epsilon);
vx_scalar s_opt_flow_num_iterations = vxCreateScalar(context_, VX_TYPE_UINT32, ¶ms_.opt_flow_num_iterations);
NVXIO_CHECK_REFERENCE(s_opt_flow_num_iterations);
assert(params_.opt_flow_use_initial_estimate == 0);
vx_bool use_initial_estimate = vx_false_e;
vx_scalar s_opt_flow_use_initial_estimate = vxCreateScalar(context_, VX_TYPE_BOOL, &use_initial_estimate);
NVXIO_CHECK_REFERENCE(s_opt_flow_use_initial_estimate);
请注意,必须将对应于值类型的枚举值传递给vxCreateScalar。在不匹配的情况下,这可能会导致很难找到的错误,例如,当我们需要32位整数,而传递32位浮点类型而不是32位整数。由于vx_size是size_t的包装,我们使用static_cast转换opt_flow_win_size,这个数是浮点而不是整数,是不确定的。
4. 使用相应的visionworks函数。
NVX_TIMER(build_pyramid, "BuildPyramid");
NVXIO_SAFE_CALL(vxuGaussianPyramid(context_,
gray,
current_pyr_
)
);
NVX_TIMEROFF(build_pyramid);
NVX_TIMER(optical_flow, "OpticalFlow");
NVXIO_SAFE_CALL(vxuOpticalFlowPyrLK(context_,
latest_pyr_,
current_pyr_,
vx_points_,
vx_points_,
vx_corresponding_points_,
VX_TERM_CRITERIA_BOTH,
s_opt_flow_epsilon,
s_opt_flow_num_iterations,
s_opt_flow_use_initial_estimate,
static_cast<vx_size>(params_.opt_flow_win_size)
)
);
NVX_TIMEROFF(optical_flow);
5. 将函数输出数据从visionworks数据类型转换为opencv类型。
从算法中可以清楚地看出,只需要corresponding_points_ and status_ arrays就可以进行进一步的运算。用vx_corresponding_points_ array填充它们:
status_.clear();
corresponding_points_.clear();
vx_size stride = 0;
vx_size num_items = 0;
vxQueryArray(vx_corresponding_points_, VX_ARRAY_ATTRIBUTE_NUMITEMS, &num_items, sizeof(num_items));
corresponding_points_.resize(num_items);
status_.resize(num_items);
void *base;
vx_map_id map_id;
NVXIO_SAFE_CALL(vxMapArrayRange(vx_corresponding_points_, 0, num_items, &map_id, &stride, (void**)&base, VX_READ_ONLY, VX_MEMORY_TYPE_HOST, 0));
for (vx_size i = 0; i < num_items; i++)
{
nvx_keypointf_t cur_item = vxArrayItem(nvx_keypointf_t, base, i, stride);
corresponding_points_[i].x = cur_item.x;
corresponding_points_[i].y = cur_item.y;
status_[i] = cur_item.tracking_status;
}
NVXIO_SAFE_CALL(vxUnmapArrayRange(vx_corresponding_points_, map_id));
6. 释放申请的objects。
vxReleaseImage(&gray);
vxReleaseScalar(&s_opt_flow_epsilon);
vxReleaseScalar(&s_opt_flow_num_iterations);
vxReleaseScalar(&s_opt_flow_use_initial_estimate);
总结
下面来看一下处理性能
opencv版本:
FastCorners : 1.047 ms
ColorConvert : 0.201 ms
BuildPyramid : 0.779 ms
OpticalFlow : 11.407 ms
FindHomography : 1.556 ms
WarpPerspective : 2.293 ms
TOTAL : 17.397 ms
block for change with OpenCV: 12.194 ms
VisionWorks version results (replaces BuildPyramid and OpticalFlow):
FastCorners : 1.194 ms
ColorConvert : 0.215 ms
BuildPyramid : 0.307 ms
OpticalFlow : 4.098 ms
FindHomography : 2.608 ms
WarpPerspective : 2.978 ms
TOTAL : 11.899 ms
changed block with Visionworks : 4.505 ms
VisionWorks version results (replaces all OpenCV functions):
FastCorners : 0.802 ms
ColorConvert : 0.124 ms
BuildPyramid : 0.131 ms
OpticalFlow : 4.411 ms
FindHomography : 1.249 ms
WarpPerspective : 0.115 ms
TOTAL : 7.170 ms
与OpenCV版本相比,这给出了大约2.5X的加速。
未完。。。。。