https://www.hahack.com/wiki/opencv-video.html#
参考资料:
使用 CV::VideoCapture
来读取视频序列。
#include
#include
#include
#include
#include
int main()
{
// Open the video file
cv::VideoCapture capture("../stomp.avi");
// check if video successfully opened
if (!capture.isOpened())
return 1;
// Get the frame rate
double rate = capture.get(CV_CAP_PROP_FPS);
bool stop(false);
cv::Mat frame; // current video frame
cv::namedWindow("Extracted Frame");
// Delay between each frame in ms
// corresponds to video frame rate
int delay = 1000 / rate;
// for all frames in video
while (!stop){
// read next frame if any
if (!capture.read(frame))
break;
cv::imshow("Extracted Frame", frame);
// introduce a delay
// or press key to stop
if (cv::waitKey(delay) >= 0)
stop = true;
}
// Close the video file
// Not required since called by destructor
capture.release();
}
也可以通过类似的方法读入摄像头捕捉的视频,要改动的地方仅仅是将上面的视频文件名改为摄像头的 ID,默认的摄像头 ID 为 0。
使用 CV::VideoWriter
来写入视频。
#include
#include
#include
#include
#include
#include
#include
#include
using namespace cv;
using namespace std;
int main()
{
VideoCapture cap;
cap.open(0);
namedWindow("MyVideo", 1);
double dWidth = cap.get(CV_CAP_PROP_FRAME_WIDTH);
double dHeight = cap.get(CV_CAP_PROP_FRAME_HEIGHT);
Size frameSize(static_cast(dWidth), static_cast(dHeight));
VideoWriter oVideoWriter("MyVideo.avi", CV_FOURCC('P','I','M','1'), 20, frameSize, true); // initialize the VideoWriter objetct
while (1) {
Mat frame;
bool bSuccess = cap.read(frame);
if (!bSuccess){ // if not success, break loop
cout << "ERROR: cannot read a frame from video file" << endl;
break;
}
oVideoWriter.write(frame);
imshow("MyVideo", frame); // show the frame in "MyVideo" window
if(waitKey(10) == 27) {// wait for ESC key
cout << "ESC key is pressed by user" << endl;
break;
}
}
return 0;
}
均值漂移(mean-shift)
mean-shift 算法是一种在一组数据的密度分布中寻找局部极值的稳定 [1] 的方法。若分布是连续的,处理过程就比较容易,这种情况下本质上只需要对数据的密度直方图应用爬山算法即可。然而,对于离散的数据集,这个问题在某种程度上是比较麻烦的。
mean-shift 算法的步骤如下:
OpenCV 提供 cv::meanshift()
函数来进行 mean-shift 算法跟踪。
int cv::meanShift(InputArray probImage, Rect& window, TermCriteria criteria)
其中,
probImage
- 图像直方图反投影后的结果;window
- 初始的查找窗口,即要跟踪的区域;criteria
- 迭代搜索算法的终止条件,主要由 mean-shift 移动的最大迭代次数和可视为窗口位置收敛的最小移动距离组成。对于第一个参数 probImage
,可以直接使用 cv::calcBackProject()
得到的结果。但《OpenCV 2 Computer Vision Application Programming Cookbook》建议先把图像转换到 HSV 颜色空间,然后使用 Hue 单通道的直方图的反投影变换结果作为 probImage
;
获取彩色图像的 Hue 通道的直方图算法实现如下:
// Computes the 1D Hue histogram with a mask.
// BGR source image is converted to HSV
cv::MatND getHueHistogram(const cv::Mat &image,
int minSaturation = 0) {
cv::MatND hist;
// Convert to HSV color space
cv::Mat hsv;
cv::cvtColor(image, hsv, CV_BGR2HSV);
// Mask to be used (or not)
cv::Mat mask;
if (minSaturation > 0) {
// Spliting the 3 channels into 3 images
std::vector v;
cv::split(hsv, v);
// Mask out the low saturated pixels
cv::threshold(v[1], mask, minSaturation,
255, cv::THRESH_BINARY);
}
// Prepare arguments for a 1D hue histogram
hranges[0]= 0.0;
hranges[1]= 180.0;
channels[0]= 0; // the hue channel
// Compute histogram
cv::calcHist(&hsv,
1, // histogram of 1 image only
channels, // the channel used
mask, // no mask is used
hist, // the resulting histogram
1, // it is a 1D histogram
histSize, // number of bins
ranges // pixel value range
);
return hist;
}
使用 cv::meanshift()
函数在两幅图像间跟踪某一物体的步骤如下:
cv::threshold()
,去掉低饱和度的像素,以保证较高的饱和度;cv::bitwise_and()
,去除结果中低饱和度的像素;cv::meanShift()
,得到新的跟踪窗口。实现如下:
int main()
{
// Read reference image
cv::Mat image= cv::imread("../baboon1.jpg");
if (!image.data)
return 0;
// Define ROI
cv::Mat imageROI= image(cv::Rect(110,260,35,40));
cv::rectangle(image, cv::Rect(110,260,35,40), cv::Scalar(0,0,255));
// Display image
cv::namedWindow("Image 1");
cv::imshow("Image 1",image);
// Get the Hue histogram
int minSat=65;
ColorHistogram hc;
cv::MatND colorhist= hc.getHueHistogram(imageROI, minSat);
ObjectFinder finder;
finder.setHistogram(colorhist);
finder.setThreshold(0.2f);
// Second image
image= cv::imread("../baboon3.jpg");
// Display image
cv::namedWindow("Image 2");
cv::imshow("Image 2",image);
// Convert to HSV space
cv::Mat hsv;
cv::cvtColor(image, hsv, CV_BGR2HSV);
// Split the image
vector v;
cv::split(hsv,v);
// Eliminate pixels with low saturation
cv::threshold(v[1],v[1],minSat,255,cv::THRESH_BINARY);
cv::namedWindow("Saturation");
cv::imshow("Saturation",v[1]);
// Get back-projection of hue histogram
int ch[1]={0};
cv::Mat result= finder.find(hsv,0.0f,180.0f,ch,1);
cv::namedWindow("Result Hue");
cv::imshow("Result Hue",result);
// Eliminate low stauration pixels
cv::bitwise_and(result,v[1],result);
cv::namedWindow("Result Hue and raw");
cv::imshow("Result Hue and raw",result);
cv::Rect rect(110,260,35,40);
cv::rectangle(image, rect, cv::Scalar(0,0,255));
cv::TermCriteria criteria(cv::TermCriteria::MAX_ITER,10,0.01);
cout << "meanshift= " << cv::meanShift(result,rect,criteria) << endl;
cv::rectangle(image, rect, cv::Scalar(0,255,0));
// Display image
cv::namedWindow("Image 2 result");
cv::imshow("Image 2 result",image);
cv::waitKey();
return 0;
}
《The OpenCV Reference Manual》里还建议再对反投影的结果进行降噪处理。例如,可以先使用 cv::findContours()
检索到一些闭合的轮廓边,使用 cv::contourArea()
去掉一些面积较小的轮廓边,再使用 cv::drawContours()
填充剩下的轮廓边。
camshift
camshift 是“continuously adaptive mean-shift”的缩写,顾名思义就是 mean-shift 的一个改进版本,它的实现基于 meanshift ,而与 meanshift 的最大区别在于窗口的大小和朝向是可变的。如果有一个易于分割的分布(例如保持紧密的人脸特征),此算法可以根据人在走近或远离摄像机时脸的尺寸而自动调整窗口的尺寸。
OpenCV 提供 cv::Camshift()
函数来进行 mean-shift 算法跟踪。
RotatedRect CamShift(InputArray probImage, Rect& window, TermCriteria criteria)
该函数将返回一个经过旋转的矩形结构体 RotatedRect
,该结构体包含了目标的位置、大小和朝向信息。可以通过 RotatedRect::boundingRect()
函数获得查找窗口的下一个位置。
?:
OpenCV 自带的示例程序中包含一个应用 camshift 捕捉彩色物体的示例 camshiftdemo.c 。
Harris 角点
经典版本
void cornerHarris(InputArray src, // input
OutputArray dst, // output
int blockSize, // neighborhood size
int ksize, // aperture size
double k, // Harris parameter
int borderType=BORDER_DEFAULT )
示例:
// Detect Harris Corners
cv::Mat cornerStrength;
cv::cornerHarris(image,cornerStrength,
3, // neighborhood size
3, // aperture size
0.01); // Harris parameter
// threshold the corner strengths
cv::Mat harrisCorners;
double threshold= 0.0001;
cv::threshold(cornerStrength,harrisCorners,
threshold,255,cv::THRESH_BINARY_INV);
原图:
结果图:
与原图结合:
改进版本
用上面的方法得到的 Harris 角点在图像中的分布不是很均匀,一个改进的方法是使用 cv::goodFeaturesToTrack()
方法(听名字就很彪悍),用它可以获得更加 strong 的角点:
void goodFeaturesToTrack(InputArray image, // input 8-bit or floating-point 32-bit, single-channel image
OutputArray corners, // output vector of detected corners.
int maxCorners, // maximum number of corners to return
double qualityLevel, // quality level
double minDistance, // minimum allowed distance between points
InputArray mask=noArray(),// optional region of interest
int blockSize=3, // size of an average block
bool useHarrisDetector=false, // whether to use a classical Harris detector
double k=0.04 ) // free parameter of the Harris detector
示例:
// Compute good features to track
std::vector corners;
cv::goodFeaturesToTrack(imaeg, corners,
500, // maximum number of corners to return
0.01, // quality level
10); // minimum allowed distance between points
结果:
这种方法得到的 Harris 角点在分布均匀程度上得到了很明显的改进。不过,这种改进的方案计算复杂度也更高,因为它需要对检测到的特征点按照 Harris 得分进行排序。另外,如果把第 8 个参数 useHarrisDetector
的值设为 true,则用的将是经典的 Harris 检测器。
通用接口
为了方便使用这些特征检测算法,OpenCV 2 提供了特征点检测的通用接口,封装在抽象类 cv::FeatureDetector
里:
class CV_EXPORTS FeatureDetector
{
public:
virtual ~FeatureDetector();
void detect( const Mat& image, vector& keypoints,
const Mat& mask=Mat() ) const;
void detect( const vector& images,
vector >& keypoints,
const vector& masks=vector() ) const;
virtual void read(const FileNode&);
virtual void write(FileStorage&) const;
static Ptr create( const string& detectorType );
protected:
...
};
其中:
create()
函数用于创建一个特征检测器,其参数 detectorType
可以是以下几种:
detect()
函数用于进行特征点检测;read()
和 write()
函数用于将检测到的特征点进行文件读/写。为了方便存储不同的特征点的信息,OpenCV 还为这个接口还定义了一个 Keypoint
类,用于存储特征点的所有相关属性。例如,对于 Harris 角点,该类的实例将会用来储存检测得到的特征点的位置。
另外,OpenCV 为每种特征点提供了从 cv::FeatureDetector
继承而来的子类。例如,改进的 Harris 角点对应子类 cv::GoodFeatureToTrackDetector
。使用方法如下:
// vector of keypoints
std::vector keypoints;
// Construction of the Good Feature to Track detector
cv::GoodFeatureToTrackDetector gftt (
500, // maximum number of corners to be returned
0.01, // quality level
10); // minimum allowed distance between points
// point detection using FeatureDetector method
gftt.detect(image, keypoints);
OpenCV 还提供了一个通用方法 cv::drawKeyPoints
用于在图像上绘制得到的特征点:
cv::drawKeyPoints(image, // original image
keypoints, // vector of keypoints
image, // the output image
cv::Scalar(255, 255, 255), // keypoint color
cv::DrawMatchesFlags::DRAW_OVER_OUTIMG); // drawing flag
这些类的使用方法大同小异。
FAST 特征点是为了提高检测速度而设计的。使用示例:
// vector of keypoints
std::vector keypoints;
// Construction of the Fast feature detector object
cv::FastFeatureDetector fast(
40); // threshold for detection
// feature point detection
fast.detect(image, keypoints);
// Draw key points
cv::drawKeyPoints(image, // original image
keypoints, // vector of keypoints
image, // the output image
cv::Scalar(255, 255, 255), // keypoint color
cv::DrawMatchesFlags::DRAW_OVER_OUTIMG); // drawing flag
SURF(Speeded Up Robust Features) 特征点是一种尺度不变,且运算效率快的特征点。
示例:
// vector of keypoints
std::vector keypoints;
// Construct the SURF feature detector object
cv::SurfFeatureDetector surf(
2500.); // threshold
// Detect the SURF features
surf.detect(image, keypoints);
// Draw the keypoints with scale and orientation information
cv::drawKeypoints(image, // original image
keypoints, // vector of keypoints
featureImage, // the resulting image
cv::Scalar(255,255,255), // color of the points
cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS); //flag
对另一幅尺度不同的图使用 SURF 特征检测,得到结果如下:
仔细对比两幅图,可以发现当图像的尺度发生变化后,两幅图像存在对应的圆圈也会跟着改变大小。尽管不是全部特征点都有对应,但通常存在对应的特征点的数量已经足够用在两幅图像间的比配。
SURF 的一个发展是另一个著名的特征点 —— SIFT (Scale-Invariant Feature Transform) 。
使用示例:
// vector of keypoints
std::vector keypoints;
// Construct the SURF feature detector object
cv::SiftFeatureDetector sift{
0.03, // feature threshold
10.); // threshold to reduce
// sensitivity to lines
// Detect the SURF features
sift.detect(image, keypoints);
}
结果和 SURF 提取的结果很相似:
SIFT 特征点比 SURF 更精确,不过速度上也慢一些。
特征点的一大用处是用来进行特征匹配,例如寻找同个场景在两个不同角度的照片的对应关系。
在特征匹配中,特征描述子(feature descriptors)通常是一个用于描述一个特征点的 N 维向量,且在理想情况下该向量可以对光照的变化和小幅度的形变具有不变性。另外,描述子的好坏还可以通过一个距离方程(例如欧氏距离)来比较和度量。因此,特征描述子是特征匹配算法中的一个强有力的工具。
同样,OpenCV 2 提供了一个通用的接口类 cv::DescriptorExtractor
用于构造特征描述子:
class CV_EXPORTS DescriptorExtractor
{
public:
virtual ~DescriptorExtractor();
void compute( const Mat& image, vector& keypoints,
Mat& descriptors ) const;
void compute( const vector& images, vector >& keypoints,
vector& descriptors ) const;
virtual void read( const FileNode& );
virtual void write( FileStorage& ) const;
virtual int descriptorSize() const = 0;
virtual int descriptorType() const = 0;
static Ptr create( const string& descriptorExtractorType );
protected:
...
};
其中:
create()
函数用于创建一个特征描述子,其参数 detectorType
可以是以下几种:
compute()
函数用于从一组检测得到的特征点计算特征描述子;read()
和 write()
函数用于将检测到的特征点进行文件读/写;示例:
// Construction of the SURF descriptor extractor
cv::SurfDescriptorExtractor surfDesc;
// Extraction of the SURF descriptors
cv::Mat descriptors1;
surfDesc.compute(image1,keypoints1,descriptors1);
结果是一个行数和特征点数量相同的矩阵,每一行都是一个 N 维 的描述子的 vector (以 SURF 描述子为例,N 默认为 64) 。每个 vector 可以很好的描述一个特征点周围的色值信息。两个特征点越相似,则它们的特征描述子越接近。
特征描述子在图像匹配中尤其有用。OpenCV 提供了一个最基本的匹配算法 cv::BruteForceMatcher()
,用法如下:
// Construction of the matcher
cv::BruteForceMatcher> matcher;
// Match the two image descriptors
std::vector matches;
matcher.match(descriptors1,descriptors2, matches);
同样,OpenCV 也提供了一个通用接口类 cv::DescriptorMatcher
,封装了多种匹配方法。
得到的匹配结果是一个 cv::DMatch
类型的数据结构,这个结构体是一种 pair 类型 ,每一个 pair 分别包含第一个 vector 和第二个 vector 的匹配到的元素的序号。
OpenCV 也提供了函数用于绘制匹配结果 cv::drawMatches
,下面示例将上面的代码得到的结果的前 25 个配对给绘制出来:
std::nth_element(matches.begin(), // initial position
matches.begin()+24, // position of the sorted element
matches.end()); // end position
// remove all elements after the 25th
matches.erase(matches.begin()+25, matches.end());
// Visualize matches
cv::Mat imageMatches;
cv::drawMatches(
image1,keypoints1, // 1st image and its keypoints
image2,keypoints2, // 2nd image and its keypoints
matches, // the matches
imageMatches, // the image produced
cv::Scalar(255,255,255)); // color of the lines
结果如下:
计算机视觉领域使用的相机模型称为针孔相机模型(pin-hole camera):
基本的投影方程如下:
其中,图像的大小 (hihi) 与物体的大小 (hoho) 沿着与焦点的距离 (dodo) 成比例关系。
根据前面所提的背景知识,我们可以清楚地认识到在针孔相机模型里的关键参数就是相机的焦距和它的成像大小。另外,因为我们要处理的是数字图像,相机的像素数量也是一个相机的另一个重要参数。最后,为了计算场景中的一点在像素坐标中的位置,我们还需要一个额外的信息:考虑那条从焦点引出的与成像平面垂直的直线,我们需要知道这条线穿入成像平面的哪个像素点,这个点称为 主要点 (principal point)。逻辑上,这个主要点应该设在成像平面的中心,但实际上这个点往往会由于机械原因,难免会偏移一些像素,这就是畸变的由来。
检测棋盘角点
对相机进行畸变校正的主要思想是给相机展示一些三维空间间位置已知的点,之后寻找这些点在图像上的相应投影位置,最后根据这些对应关系计算相机的相关信息。OpenCV 建议通过从不同角度获取一个棋盘模板图像来校正相机。在这个情况下,可以利用每张棋盘图像上的角点来获取实际位置和投影位置的对应关系,从而计算相机的相关信息。一个示例棋盘模板:
从上面这张图也可以很明显的看出相机存在较严重的径向畸变(radial distortion),这种畸变现象在鱼眼镜头中非常普遍。
OpenCV 提供了一个自动检测棋盘模板中的角点的函数 cv::findChessboardCorners()
:
bool findChessboardCorners(InputArray image, // source chessboard view
Size patternSize, // number of inner corners per a chessboard row and column
OutputArray corners, // output array of detected corners
int flags=CALIB_CB_ADAPTIVE_THRESH+CALIB_CB_NORMALIZE_IMAGE
)
调用它通常只需要提供一幅图像以及棋盘的大小信息(水平和竖直的内角点的数量,如上图中分别是 6 和 4),该方法会返回这些棋盘角点在图像中的位置:
// output vectors of image points
std::vector imageCorners;
// number of corners on the chessboard
cv::Size boardSize(6, 4);
// Get the chessboard corners
bool found = cv::findChessboardCorners(image,
boardSize,
imageCorners);
绘制检测得到的结点
OpenCV 也提供了一个函数用于将检测到的结点画到棋盘图像上:
// Draw the corners
cv::drawChessboardCorners(image,
boardSize, imageCorners,
found); // corners have been found
这些角点间的线条可以表示这些角点在 vector 中的顺序。
校正相机
当检测足够多不同角度的棋盘图像后(大概 10 ~ 20 张),就可以对相机进行校正了,OpenCV 提供了一个函数 cv::calibrateCamera()
进行校正:
double calibrateCamera(InputArrayOfArrays objectPoints, // a vector of vectors of calibration pattern points
InputArrayOfArrays imagePoints, // a vector of vectors of the projections of calibration pattern points.
Size imageSize, // size of the image
InputOutputArray cameraMatrix, // output 3x3 floating-point camera matrix
InputOutputArray distCoeffs, // output vector of distortion coefficients
OutputArrayOfArrays rvecs, // output vector of rotation vectors
OutputArrayOfArrays tvecs, // output vector of translation vectors e
int flags=0, // flags
TermCriteria criteria=TermCriteria( // termination criteria
TermCriteria::COUNT+TermCriteria::EPS,
30, DBL_EPSILON) )
调用该函数将可以得到两个重要的输出:相机矩阵 cameraMatrix
和畸变系数 distCoeffs
。
为了方便,可以将上面的所有步骤封装成一个类 CameraCalibrator :
class CameraCalibrator {
// input points
std::vector> objectPoints;
std::vector> imagePoints;
// output Matrices
cv::Mat cameraMatrix;
cv::Mat distCoeffs;
// flag to specify how calibration is done
int flag;
// used in image undistortion
cv::Mat map1,map2;
bool mustInitUndistort;
public:
CameraCalibrator() : flag(0), mustInitUndistort(true) {};
// Open the chessboard images and extract corner points
int addChessboardPoints(const std::vector& filelist, cv::Size & boardSize);
// Add scene points and corresponding image points
void addPoints(const std::vector& imageCorners, const std::vector& objectCorners);
// Calibrate the camera
double calibrate(cv::Size &imageSize);
// Set the calibration flag
void setCalibrationFlag(bool radial8CoeffEnabled=false, bool tangentialParamEnabled=false);
// Remove distortion in an image (after calibration)
cv::Mat remap(const cv::Mat &image);
// Getters
cv::Mat getCameraMatrix() { return cameraMatrix; }
cv::Mat getDistCoeffs() { return distCoeffs; }
};
其中:
addChessboardPoints()
函数 - 用于读入一系列的棋盘图像并检测角点;calibrate()
函数 - 用于进行相机校正,得到相机的参数矩阵和畸变系数;remap()
函数 - 用于根据相机校正结果修复图像的畸变;addPoints()
函数 - addChessboardPoints()
在检测完角点后会调用这个函数。也可自己手动调用这个函数添加已知的角点位置和对应的空间坐标点。使用示例:
int main()
{
cv::namedWindow("Image");
cv::Mat image;
std::vector filelist;
// generate list of chessboard image filename
for (int i=1; i<=20; i++) {
std::stringstream str;
str << "../chessboards/chessboard" << std::setw(2) << std::setfill('0') << i << ".jpg";
std::cout << str.str() << std::endl;
filelist.push_back(str.str());
image= cv::imread(str.str(),0);
cv::imshow("Image",image);
cv::waitKey(100);
}
// Create calibrator object
CameraCalibrator cameraCalibrator;
// add the corners from the chessboard
cv::Size boardSize(6,4);
cameraCalibrator.addChessboardPoints(
filelist, // filenames of chessboard image
boardSize); // size of chessboard
// calibrate the camera
// cameraCalibrator.setCalibrationFlag(true,true);
cameraCalibrator.calibrate(image.size());
// Image Undistortion
image = cv::imread(filelist[6]);
cv::Mat uImage= cameraCalibrator.remap(image);
// display camera matrix
cv::Mat cameraMatrix= cameraCalibrator.getCameraMatrix();
std::cout << " Camera intrinsic: " << cameraMatrix.rows << "x" << cameraMatrix.cols << std::endl;
std::cout << cameraMatrix.at(0,0) << " " << cameraMatrix.at(0,1) << " " << cameraMatrix.at(0,2) << std::endl;
std::cout << cameraMatrix.at(1,0) << " " << cameraMatrix.at(1,1) << " " << cameraMatrix.at(1,2) << std::endl;
std::cout << cameraMatrix.at(2,0) << " " << cameraMatrix.at(2,1) << " " << cameraMatrix.at(2,2) << std::endl;
imshow("Original Image", image);
imshow("Undistorted Image", uImage);
cv::waitKey();
return 0;
}
结果: