highgui
的常用函数: cv::namedWindow
:一个命名窗口 cv::imshow
:在指定窗口显示图像 cv::waitKey
:等待按键
image.at<>(row,colomn)[]
来取值,灰度就是uchar
,常用的RGB通道则是cv::Vec3b
,b代表ushort,s-short, i-int, f-float. at方法本身不做任何类型转换;cv::Mat
,这是一个泛型的数据结构,所以在使用at时要指定类型,也可以直接声明存储类型,如cv::Mat_
,这样在使用at(i,j)
时就不必指明结构;ptr<>
方法来直接访问内存,它返回指定类型的指针。对于连续矩阵,其长度就是矩阵的行长度,其宽度则是channels()*cols
。所以,使用指针扫描图像,格式就是:
cv::Mat image;
int nl=image.rows;
int nc=image.cols*image.channels()
for(int j=0;j<nl;j++)
uchar *data=image.ptr(j); //取得行首指针
for(int i=0;i<nc;i++)
data[i]=... //像素间距为uchar
书中这里给出了减少像素范围的算法(也就是像素位数),让每个像素值/div*div+div/2。这是一个常用的彩色图像处理时的预处理步骤。 * opencv默认存放彩色图像为BGR
顺序,即image.at[0]=blue
...;
显然,为了内存对齐等目的,数组的行宽度和实际图像像素的行宽度不一致,可以使用data
取得元素头指针,step
属性取得数组的行字节数,使用elemSize
方法取得一个像素的字节数,使用channels
方法取得通道数,使用total
方法取得总像素数;
opencv的内存是自己管理的,正常使用赋值操作,得到的是引用。使用clone
方法来进行深拷贝,使用create
工厂方法来填充矩阵(用default constructor 声明时);
可以使用data
属性取得低级指针,然后配合step
等属性|方法来进行指针低级运算,一般不推荐使用这种方法;
迭代器,使用cv::MatIterator_<>
或cv::Mat<>_::iterator
来声明,当然最好用auto
(for c++11),使用begin<>
和end<>
方法来取得头尾迭代器;只读迭代器为cv::MatConstIterator_<>
,或者cv::Mat_<>::const_iterator
;迭代器的效率不高,但是可以配合STL使用;
cv::Mat image;
auto iter=image.begin();
auto iterend=image.end();
for(;iter!=iterend;++iter)
(*iter)[0]=...
上面是使用迭代器进行遍历的过程。注意迭代器的访问速度还要快于直接使用at(i,j)
。
saturate_cast(value)
对value
实行指定type
的截断操作;cv::getTickCount()
返回从启动电脑以来的时钟滴答数,配合用来获得每秒滴答数的cv::getTickFrequency()
,可以用作一个普通的定时器;当然,也可以使用boost::timer
;row(n)
和col(n)
来取得行、列的向量引用,使用cv::Scalar()
来建立向量,使用setTo
方法可以对矩阵进行向量尺度的赋值。顺便一提,Scalar_
是固定长度为4的向量,即Vec
,而typedef Scalar_ Scalar
;cv::filter2D
来进行空间域卷积滤波:
cv::Mat_ filter_kernel(3,3,0.0f);
filter_kernel[1][1]=5.0f;
filter_kernel[0][1]=-1.0f;
filter_kernel[1][0]=-1.0f;
filter_kernel[2][1]=-1.0f;
filter_kernel[1][2]=-1.0f;
cv::filter2D(image,result,image.depth(),filter_kernel);
建立滤波核,然后应用到图像即可,支持inplace
处理;
cv::add
or cv::addWeighted
,可以应用mask(mask必须是单通道矩阵,操作仅对mask非0的像素执行);减法—cv::subtract
,乘法—cv::multiply
,除法—cv::divide
,差的绝对值—cv::absdiff
;位运算:cv::bitwise_and
,cv::bitwise_or
,cv::bitwise_xor
和cv::bitwise_not
;cv::min
和cv::max
用来计算像素极值;此外cv::sqrt
, cv::abs
,cv::cuberoot
, cv::exp
, cv::log
等函数也存在;inv
(转置),cross
(X乘),dot
(点乘),determinant
(行列式)等方法进行计算;注意所有的操作符的计算结果都会被截断,如果需要负值或者过大的值, 不能直接用操作符计算;cv::split
可以将多通道矩阵切割成多个单通道矩阵,其第二个参数是std::vector
;使用cv::merge
对切割矩阵进行合并;cv::Rect(x,y,cols,rows)
或者cv::row(xth,yth)
或cv::Range(xbegin,xend)
,或者同义的rowRange(begin,end)
方法,即可得到对应行/列的引用。另外,Range::all()
的意思同Matlab中的:
操作符。Strategy
模式,将之包裹在遵从接口的类中,使用控制器来关联类和作用的对象;cv::cvtColor(image,result,flag)
用来完成此工作,flag是系列CV_XX2XX
的色彩转换常数,支持in-place
操作,其中CV_BGR2gray
是彩色转灰度的参量;注意:opencv自由一些常见的颜色空间转换函数,实际上的颜色空间多到蛋疼的地步;cv::calcHist()
,由于该函数的参数过于复杂,为简化使用,一般将之封装在提供默认参数的类中。对于单通道图像,可以使用:
class Histogram1D{
private:
int histSize[1]; //像素值的有效值个数
float hranges[2]; //像素值有效值的上下限
const float *ranges[1]; //指向hranges
int channels[1]; //通道数
public:
Histogram1D{
HistSize[0]=256;
hranges[0]=0.0;
hranges[1]=255.0;
ranges[0]=hranges;
channels[0]=1;
}
cv::MatND getHistogram(const cv::Mat& image)
{
cv::MatND hist;
cv::calcHist(&image,1,channels,cv::Mat(),hist,1,histSize,ranges);
return hist;
}
//描绘直方图对应的点状图(连续图像)
cv::Mat getHistogramImage(const cv::Mat& image)
{
cv::MatND hist=getHistgram(image);
double maxVal=0.0;
double minVal=0.0;
cv::minMaxLoc(hist, &minVal,&maxVal,0,0);
cv::Mat histImg(histSize[0],histSize[0],CV_8U,cv::Scalar(255));
int hpt=static_cast(0.9*histSize[0]);
for(int h=0;h<histSize[0];++h)
{
float binVal=hist.at(h);
int intensity=static_cast(binVal*hpt/maxVal);
cv::line(histImg,cv::Point(h,histSize[0]),
cv::Point(h,histSize[0]-intensity),
cv::Scalar::all(0));
}
return histImg;
}
};
cv::threshold
取阈值,进行二值化处理。cv::SparseMat
创建稀疏矩阵,减少内存占用量(含有大量0值时);cv::LUT
进行转换;cv::equalizeHist
进行处理,实际上直方图均衡化是直方图规格化的一个特例;cv::normalize
进行直方图归一化;cv::calcBackProject
得到一个概率分布图(与目标直方图分布的匹配程度),对该图进行二值化,即可得到近视的目标匹配效果。需要注意:如果使用灰度直方图,目标匹配的效果会很差,必须使用彩色直方图。Mean-Shift
算法(均值漂移),其跟踪依据(特征参数)是概率密度梯度函数,其终止条件可以是相似度或最大迭代次数。Mean-Shift需要把图像转换到hue颜色空间,其特征使用了大量颜色信息,因此不能把源图像灰度化;cv::compareHist
函数,其中第三个参数指定比较的算法,注意在比较之前一般要减少图像的色彩度。cv::erode
,膨胀:cv::dilate
,支持in-place
变换。腐蚀是用将当前像素值用形态核中最小的值代替,膨胀则是用最大值。默认使用3*3形态核;cv::morphologyEx
函数,指定第三个参数为cv::MORPH_CLOSE
则为闭操作(先膨胀再腐蚀,填补白色前景中的孔洞),cv::MORPH_OPEN
为开操作(先腐蚀再膨胀,移除前景中的小物体),开运算和闭运算都是幂等操作。参数cv::MORPH_GRADIENT
用来梯度化处理(辅以二值化),一般用来做边缘检测;cv::watershed
完成这一操作,如果用于物体探测,我们首先需要知道一些确切属于某区域|物体的点,将其作为掩膜带入函数,函数会逐步升高水平线直到达到掩膜区域给定的边界;cv::grabCut
,填充最大迭代次数和算法的flag即可。具体原理这里不记录。cv::blur
,均值滤波;cv::GaussianBlur
,高斯滤波;cv::medianBlur
,中值滤波;cv::pyrUp
, cv::pyrDown
,以及更通用的cv::resize
;cv::Sobel
,该算子是方向性的,可选择用于垂直或水平方向。如果选择生成8位灰度图像,产生的效果就是常用绘图软件中的“浮雕”效果。我们综合两个方向的结果,再进行一个灰度变换,就可以得到边缘检测结果。Sobel
算子可以被看做图像在垂直|水平方向上某个参数的度量,这个参数就是所谓“梯度”,梯度指向灰度变化最快的方向,其长度就是欧拉距离。为了减少计算量,这里直接用水平方向和垂直方向两者绝对值的和来近似。Sobel
外,还有其他梯度算子,不同之处当然在于滤波核。cv::Laplacian
对图像做拉普拉斯变换,本质上也是一个高通滤波器,类似Sobel,除了不用指出方向。拉普拉斯变换对噪声非常敏感。 在拉普拉斯变换结果中,从正到负(或者相反)的地方意味着图像的边界。可以通过图像减去其拉普拉斯变换的结果增强对比度;cv::Canny
使用了两个阈值来过滤所需的边缘,从而可以提取出所需物体的轮廓。Canny
算子依赖于Sobel
算子(或其他边缘提取算子)。方法的核心是先利用低阈值得到包含正确边缘的图像,利用高阈值来提取包含重要轮廓的部分,然后通过一些计算保留了第二个阈值中限定的重要轮廓,同时尽量移除第一个阈值中不重要的部分。这种算法属于“双阈值门限”算法。
Hough变换用来探测图像中的直线,有两个版本: > cv::HoughLines
用来检测经过边缘检测的二值图像,通过指定需探测直线的角度范围即可; >cv::HoughLinesP
是概率性霍夫变换,增加了两个参数用来表明探测直线的最小长度和连续物体的最小像素间距;
除了直线检测外,还有一些函数专用于检测其他简单形状,包括圆(HoughCircles)和其他不规则形状;
使用cv::fitLine
对指定点集进行直线拟合,cv::fitEllipse
对指定点集进行椭圆拟合;
使用cv::findContours
在二值化的图像中寻找连续像素组成物体的轮廓。输入一个包含轮廓的二值图像,输出是一个轮廓(点集)的集合(std::vectorcv::drawContours
描绘出这些轮廓;
有时候需要将轮廓包围起来做标识,使用cv::boundingRect
(矩形),cv::minEnclosingCircle
(圆), cv::approxPolyDP
(多边形),cv::convexHull
(凸包), cv::Moments
(一阶矩),cv::minAreaRect
(最小旋转矩形), cv::contourArea
估算面积,其构造参数都是轮廓。
使用轮廓可以进行形态上的特征分类。cv::Moments
有一堆参数,其中质心就是 (\frag{m_{10}}{m_{00}},\frag{m_{01}{m_{00}})(\frag{m_{10}}{m_{00}},\frag{m_{01}{m_{00}}) 其他的参照公式。
cv::pointPolygonTest
可以用来检测一个点是否在轮廓内;cv::matchShapes
用来评估轮廓的相似度,可选算法有3种。
物体的轮廓是物体最基本的特征之一,在一些简单的object detection算法应用中,使用轮廓本身的匹配,或者轮廓相关的特征的匹配,就可以达到识别物体的目的。cv::SimpleBlobDetector
就是利用了这个特性。
Harris corners探测:cv::cornerHarris
,用来探测角点,其原理是先计算其平均灰度变化的梯度方向(使用Sobel边缘检测算子),然后在计算该方向垂直方向的平均灰度变化,如果变化率也很高,那么就是一个角点。其对应矩阵特征是其协方差矩阵拥有高于指定阈值的最小特征值;cv::goodFeaturesToTrack
,通过指定角点的一些特征(最大数量、质量等级、最小点距)来进行Harris corner探测,使特征点的分布更加均匀。角点具有一定的尺度不变形,并且运算量不大,在object detection中是一种很好的探测特征。
cv::FeatureDetector
是一个通用的特征探测抽象类(继承自cv::Algorithm
),规定了接口detect
方法,用于返回满足条件的特征集(std::vector
),cv::goodFeaturesToTrackDetector
就是这个抽象类的一个子类(顾名思义,是cv::goodFeaturesToTrack
函数的一个包装类。)
cv::FastFeatureDetector
用来完成快速(Harris)特征检测,这个也是cv::FeatureDetector
的一个子类,这是一种估算,因此精确度略低,但速度很快,适用于对实时性要求较高的场合。另外cv::drawKeypoints
可以用来直接描绘关键点。
尺度无关的特征检测。书里面介绍了SURF(Speeded Up Robust Features)特征检测,cv::SurfFeatureDetector
是另一个cv::FeatureDetector
的子类。此外还提及了SIFT检测算法,与SURF相比,该算法的精细度更高,对应的,速度较低。
SURF是非常重要的特征检测算子,因为摄像头的角度如果不是垂直向下的,物体的尺度很难保持不变。SURF能够计算出尺度不变的特征,同时保持较快的计算速度,能够在一定程度上兼顾准确度和实时性要求。
除了上文中的特征检测子以外,opencv还实现了一大堆其他的检测子,包括:STAR
,ORB
,BRISK
,MSER
,Dense
和SimpleBlob
等,其中SimpleBlob
最简单。
利用直方图分布的Mean-shift算法也是一种特征。
cv::DescripterExtractor
和cv::DescriptorMatcher
也是抽象类,前者用于特征提取,有一个工厂函数,对应前面的特征检测子,使用方法compute
计算特征;后者使用match
匹配。match
方法的结果是cv::DMatch
的集合,Struct DMatch包含queryIdx, trainIdx和imgIdx,以及一个distance表示两个点间距。可以使用cv::drawMatches
来描绘这些匹配。
cv::DescripterExtractor *extractor=new cv::SurfDescriptor();
cv::Mat descriptors1,descriptors2;
extractor->compute(image1,keypoints1,descriptors1);
extractor->compute(image2,keypoints2,descriptors2);
std::vector matches;
cv::DescriptorMatcher *matcher=new cv::BruteForceMatcher<cv::L2>();
matcher->match(descriptors1,decriptors2,matches);
std::nth_element(matches.begin(),matches.begin()+24,
matches.end());
matches.erase(matches.begin()+25,matches.end());
cv::drawMatches(image1,keypoints1,
image2,keypoints2,
matches,
imageMatches,
cv::Scalar(255,255,255));
涉及3D建模,本章暂略。
//用视频文件进行初始化
cv::VideoCapture capture("../xxx.avi");
//检测是否打开成功。不过这里无法判断打开不成功的原因
if(!capture.isOpened())
{
return 1;
}
//帧率, 使用set可以定位
double rate=capture.get(CV_CAP_PROP_FPS);
bool stop(false);
cv::Mat frame;
cv::namedWindow("Extracted Frame");
int delay=1000/rate;
while(!stop)
{
//读取下一帧,也可以用capture>>frame,
//capture.grab(), capture.retrieve(frame)
if(!capture.read(frame))
{
break;
}
cv::imshow("Extracted Frame",frame);
//延迟,一般和视频的帧率保持一致
if(cv::waitKey(delay)>=0)
stop=true;
}
//可以不用,因为析构自动释放
capture.release();
}
计算机里面必须有对应的解码器才能解码视频流。对于专用视频流,opencv就无能为力了。必须自己使用正确的SDK进行图像提取,然后交给opencv进行处理。
写和读类似,用的是cv::VideoWriter
类(说起来,对应的名字是VideoReader岂不是更好)。方法open
需要指定输出文件名、编码方式(一个四字节整数)、帧率、帧大小和是否彩色。这里值得注意的是4字节的编码方式,貌似opencv中还没有对应的辅助函数,要自己写。传入-1,会弹出窗口让你选择编码方式,也可以趁机看一下系统支持哪些编码方式。
class VideoProcessor{
private:
cv::VideoWriter writer;
std::string outputFile;
int currentIndex;
int digits;
std::string extension;
bool setOutput(const std::string &filename,
int codec=0,double frameRate=0.0,
bool isColor=true)
{
outputFile=filename;
extention.clear();
if(frameRate==0.0)
frameRate=getFrameRate();
char c[4];
if(codec==0)
{
codec=getCodec(c);
}
return writer.open(outputFile,codec,frameRate,
getFrameSize(),isColor);
}
int getCodec(char codec[4]){
if(images.size()!=0)return -1;
union{
int value;
char code[4];} returned;
returned.value=static_cast(
capture.get(CV_CAP_PROP_FOURCC));
codec[0]=returned.code[0]; //这里得到编码的字符表示
codec[1]=returned.code[1];
codec[2]=returned.code[2];
codec[3]=returned.code[3];
return returned.value;
}
void writeNextFrame(cv::Mat& frame)
{
if(extension.length())
{
std::stringstream ss;
ss<<outputFile<<std::setfill('0')
<<std::setw(digits)<<currentIndex++
<<extension;
}else{
writer.write(frame);
}
}
bool setOutput(const std::string& filename,
const std::string &ext,
int numberOfDigits=3,
int startIndex=0)
{
if(numberOfDigits<0)return false;
outputFile=filename;
extension=ext;
digits=numberOfDigits;
currentIndex=startIndex;
return true;
}
void run()
{
//...
while(!isStoped())
{
if(outputFile.length()!=0)
writeNextFrame(output);
if(windowNameOutput.length()!=0)
cv::imshow(windowNameOutput,output);
if(delay>=0 && cv::waitKey(delay)>=0);
stopIt();
if(frameToStop>=0 && getFrameNumber()==frameToStop)
stopIt();
}
}
}
视频打开后,就可以使用write
方法将视频帧写入文件(也可以写成连续的图像文件)。
光流法使用示例:
class FeatureTracker : public IFrameProcessor{
//当前帧
cv::Mat gray;
//上一帧
cv::Mat gray_prev;
//两帧的特征点
std::vector points[2];
//初始特征,绘图使用
std::vector initial;
//未过滤的当前特征
std::vector features;
//下面是过滤使用和相关函数使用的参数
int max_count;
double qlevel;
double minDist;
std::vector status;
std::vector err;
public:
FeatureTracker():
max_count(500),qlevel(0.01),minDist(10.){}
void process(cv::Mat &frame,cv::Mat &output)
{
cv::cvtColor(frame,gray,CV_BGR2GRAY);
//第一次使用
if(addNewPoints())
{
//探测特征点
detectFeaturePoints();
//插入特征点
points[0].instert(points[0].end(),
features.begin(),features.end());
initial.insert(initial.end(),
features.begin(),features.end());
}
if(gray_prev.empty())
gray.copyTo(gray_prev);
//光流跟踪
cv::calcOpticalFlowPyrLK(
gray_prev,gray,
points[0],
points[1], //匹配的特征点
status,
err);
int k=0;
for(int i=0;i<points[1].size();i++){
if(acceptTrackpoint(i)){ //过滤特征点
initial[k]=initial[i];
points[1][k++]=points[1][i];
}
}
points[1].resize(k);
initial.resize(k);
handleTrackPoints(frame,output); //处理轨迹
std::swap(points[1],points[0]);
cv::swap(gray_prev,gray);
}
//特征点用了角点探测
void detectFeaturePoints(){
cv::goodFeaturesToTrack(gray,
features,
max_count,
qlevel,
minDist);
}
//这里只在开始的时候需要探测特征点,后面都是跟踪
bool addNewPoints(){
return points[0].size()<=10;
}
//过滤特征点
//两个特征点集对应
bool acceptTrackedPoint(int i){
return status[i] &&
(abs(points[0][i].x-points[1][i].x)+
abs(points[0][i].y-points[1][i].y))>2;
}
//描绘对应点
void handleTrackedPoints(cv::Mat &frame,
cv::Mat &output){
for(int i=0;i<points[1].size();i++)
{
cv::line(output,
initial[i],
points[1][i],
cv::Scalar(255,255,255));
cv::circle(output,points[1][i],3,
cv::Scalar(255,255,255),-1);
}
}
光流法是基于光流场的灰度变化趋势推算下一帧可能的位置,因此不是角度无关的,计算结果和摄像机与物体间的相对角度、位置都有关系。
FeatureDetector
,背景提取也有一个公用抽象类BackgroundSubtractor
,有一个虚函数getBackgroundImage
用于取出当前的背景图片,重载了函数调用操作符;现阶段opencv有以下两个实现: BackgroundSubtractorMOG
是高斯混合算法的一种实现,BackgroundSubtractorMOG2
也是一种高斯混合算法的实现,但是数学模型不一致。
FROM: http://www.cnblogs.com/livewithnorest/p/3397103.html