openCV3-api
本笔记主要用来记录openCV3中常用的api,大多数都是笔者在实际学习过程中用到的,可以说是经验总结,对于openCV的学习方法主要还是要理论+实战才是正道.
在本笔记中,把api按照项目场景进行分类,其中第一章是描述的openCV的基础api比如图像创建、读取、颜色空间转换等,第二章则是滤波、阈值、采样,第三章则主要针对的是形态学操作,第四章是边缘、轮廓检测, 第五章是模版匹配,第六章是目标追踪,循序渐进从传统方法到深度学习方法递进.
笔记的章节并不是固定的,也不是独立的,相互之间存在重复记录,一方面为了复习,另一方面是某些章节之间存在较为紧密的联系.
笔记的章节数目也不是固定的,随着学习的深入,会不断的追加.
⚠️该笔记理论设计较少,若需掌握相关理论详情,需要参考DIP数字图像处理笔记,这里仅以openCV实现方法为主要学习、记忆目标.
第一章 openCV基础操作
openCV基础操作主要是图片、视频的读取,图片的颜色变换,基本形状的绘制等api,还有openCV中的基础类等
1.1元素数据类型
openCV中像素点的数据类型如CV_8UC3,表示该像素点在内存中占8位,U是unsigned无符号类型,C表示channels,即为3.
同理CV_32FC12表示,该像素点在内存中占32位,是float类型,channels=12
1.2 四大基本类
Point p = Point(20,30);
Size s = Size(1920,1080);
Scalar color = Scalar(255,0,0);
Rect rect = Rect(0,0,1920,1080);
对于Rect类而言,有如下内部成员
cout<<rect.tl();
cout<<rect.br();
cout<<rect.x;
cout<<rect.y;
cout<<rect.width();
cout<<rect.height();
1.3 绘制标准形状
cv::line(Mat src, Point p1, Point p2, Scalar color, int thickness, LINE_8,0);
cv::rectangle(Mat src, Rect rect, Scalar color, int thickness, LINE_8);
cv::putText(Mat src, String text, Point p, font_type, 12, Scalar color, int thickness, LINE_8);
1.4 Mat类
Mat类可以说是openCV中使用频率最高的类,首先介绍对图片的读取和复制
Mat src (2, 2, CV_8UC3, Scalar(0,0,255);
Mat src;
Mat a = imread ("~/a.png");
Mat a(b);
Mat c = a.clone();
Mat copyImage;
a.copyTo(copyImage);
上述是对已有图片进行读取复制,这些图片的像素值是已经被相机或其他设备记录好的,那么如何创建一张像素值全是0或者全是1的像素呢? 或者指定宽高和具体像素值的图片呢?
Mat a = imread("~/a.jpeg");
Mat zero = Mat::zeros(a.size(), a.type());
Mat zero = Mat::zeros(2,2,CV_32FC3);
Mat dst;
dst.create(Size(20, 20), CV_32UC2);
Mat kernel = (Mat_<double>(3, 3)<<0, 1, 0, 1, 0, 1, 0, 1, 0);
filter2D(Mat src, Mat dst, src.depth(), kernel);
// 上述的filter2D()算子是用来进行滤波操作的
1.5 视频读取
VideoCapture cap ("~/video/a.mp4");
VideoCapture cap(0);
VideoCapture cap;
cap.open("~/video/a.mp4")
if(cap.isOpened()){}
Mat frame;
cap>>frame;
cap.read(frame);
if(cap.read(frame)){}
if(frame.empty()){}
cap.release();
上述是从视频或视频流中读取图片,判断读取内容,接下来需要解析图片参数
VideoCapture cap("~/video/a.mp4");
Mat frame;
cap>>frame;
//cap.read(frame);
int width = cap.get(CV_CAP_PROP_FRAME_WIDTH);
int height = cap.get(CV_CAP_PROP_FRAME_HEIGHT);
int frameRate = cap.get(CV_CAP_PROP_FPS);
int totalFrames = cap.get(CV_CAP_PROP_FRAME_COUNT);总帧数
1.6 图像显示、保存和转换
该节展示的是图像的读取,显示,修改保存等操作,修改主要包括的是颜色空间的转换等
Mat src = imread("~/picture/a.jpg");
namedWindow('a_window',WINDOW_AUTOSIZE);
imshow('a_window',src);
Mat gray;
cvtColor(src, gray, COLOR_BGR2GRAY);
imwrite("~/picture/a_gray.jpg");
接下来展示如何显示图片的基本参数
int channels = src.channels();
int height = src.rows;
int width = src.cols;
当图片的像素值超过255时,可以通过如下算子进行调整
src.at<uchar>(row, col) = saturate_cast(12*9);
图像转换
src.convertTo(dst, CV_32FC3);
resize(Mat src,
Mat output,
Size(newWidth, newHeight),
interPolation);
上述的interPolation参数是插值算法,主要有如下参数设置
参数 |
解释 |
备注 |
INTER_NEAREST |
最近邻插值算法 |
|
INTER_LINEAR |
双线性插值 |
默认该参数设置 |
INTER_AREA |
基于局部像素的重采样 |
|
INTER_CUBIC |
基于4x4像素邻域的3次插值算法 |
|
INTER_LANCZOS4 |
基于8x8像素邻域的Lanczos插值算法 |
|
1.7 指针访问像素
本节主要通过一个完整的例子来表示如何使用指针访问像素值,并且该示例展示了如何通过指针访问像素的方法,完成3x3的kernel对RGB图片的滤波算法
Mat src = imread("~/video/a.jpg");
Mat dst = Mat(src.size(), src.type());
int channels = src.channels();
int height = src.rows;
int width = src.cols * channels;
for ( int row = 1; row<height; row++){
const uchar * current = src.ptr<uchar>(row);
const uchar * next = src.ptr<uchar>(row + 1);
const uchar * previous = src.ptr<uchar>(row - 1);
uchar * output = dst.ptr<uchar>(row);
for (int col = channels; col < channels *( src.cols -1); col++){
*output = saturate_cast("");
*output = current[col] * 3 + next[col] - previous[col];
//该层循环内部为像素级操作
output ++;
}
}
1.8 直接访问像素点
上述是通过指针来访问像素的值显然是通过先用rowPtr = src.ptr<uchar>(row)定位到行,再使用locationPtr = rowPtr[col]来访问该行的某列的像素
直接访问像素点,根据对象类型不同,比如单通道灰度图,或者三通道RGB图,方法也不同
Mat src = imread("gray");
int src_pixel = src.at<uchar>(row, col);
Mat rgb = imread("rgb");
int b = rgb.at<Vec3b>(row, col)[0];
int g = rgb.at<Vec3b>(row, col)[1];
int r = rgb.at<Vec3b>(row, col)[2];
//像素反转
int channels = src.channels();
int height = src.rows;
int width = src.cols * channels;
for (int row = 0; row<height; row++){
for (int col =0; col < width; col++){
dst.at<uchar>(row, col) = saturate_cast(255- src.at<uchar>(row, col));
}
}
//像素反转api
bitwise_not(src, dst);
第二章 滤波阈值与采样
滤波
各种滤波API,可以通过像素级别操作完成相同的算法效果,这里只是API
滤波其实是分成了中值滤波和均值滤波,均值滤波又包含了加权均值滤波,加权均值滤波进一步演化成Gaussian加权均值滤波算法
简单说明:均值滤波就是求滑动窗口内像素平均值,中值滤波就是求中间值,高斯滤波就是按公式来求
//均值滤波
blur(Mat src, Mat dst, Size(xradius,yradius),Point(-1,-1));
//GaussianBlur
GaussianBlur(Mat src, Mat dst, Size(xradius,yradius),sigmax,sigmay);
//中值滤波 常用在处理椒盐噪声上
mediaBlur(Mat src, Mat dst, Size(xradius,yradius);
GaussianBlur的像素级别实现方法
GaussianBlur 像素级别操作
首先介绍Gaussian加权均值滤波算法,简单来说就是:
-
对滑动窗口内的每个像素加以权值,而每个滑动窗口的元素正是对应像素的权值,即通过卷积即可完成加权求和,在通过/N可得到像素加权均值,
-
一般的滑动窗口内的权值是我们自己设计的,这里采用的权值分布是高斯分布(参考正态分布,高斯分布就是正态分布),一般使用标准2维正态分布,高斯滤波把周围像素对中心像素的距离分布影响考虑进去了
由于我们往往采用的是xy独立同分布,所以ρ=0, μ是位置参数,σ是尺度参数
下面程序采用了相同的σ,实际应该有σ_x与σ_y
void getGaussianMatrix(int size, double sigma, Mat &Gauss){
Gauss.create(Size(size,size), CV_32F1);
float sum = 0.0
float center = size/2;
/*要产生一个3x3的高斯滤波器模板/kernel,要以末班中心位置为原点坐标进行取样,center用来平衡周围像素的值*/
const float PI = 3.1415926;
for (int i =0;i<size;i++){
for(int j =0;j<size;j++){
Gauss.at<float>(i,j) =
(1/(2*PI*sigma*sigma))*exp(-((i-center)*(i-center)+(j-center)*(j-center))/(2*sigma*sigma))
sum += Gauss.at<float>(i,j);
}
}
# 归一化
for (int i =0;i<size;i++){
for(int j=0;j<size;j++){
Gauss.at<float>(i,j) /= sum;
}
}
}
图像分辨率操作,上下采样与像素级减法
//上采样,就是扩大分辨率,用像素0来填充新有的像素点,然后用kernel滑动,用来把值为0的像素点和周围像素值进行近似处理,一般采用都是Gaussian kernel
pyrUp(Mat src, Mat output,Size(width,height);
//下采样,就是缩小分辨率,可以去掉1/2,或者1/4等等
pyrDown(Mat src,Mat output,Size(width,height);
//像素级别减法
subtract(Mat a, Mat b, Mat output);
//output=a-b
第三章 形态学
结构元素
getStructuringElement() 获得结构元素api, 返回一个kernel,这个kernel的元素是0或1, 可以指定1在kernel中的分布形状,比如矩形是全都是1,菱形是一个由1组成的菱形分布
Mat kernel = getStructuringElement(MORPH_RECT, Size(3,3),Point(-1,-1);
// MORPH_RECT 矩形,
// MORPH_CROSS交叉型,
// MORPH_ELLIPSE椭圆形等,这都是元素1的分布形状
膨胀,腐蚀,open,close,tophat,blackhat
Mat src = imread(“path”);
Mat output;
src.copyTo(output);
// morphology都需要的是元素为1或0的kernel,所以直接用getStructruingElement()
Mat structElement = getStructuringElement(MORPH_RECT,Size(3,3),Point(-1,-1));
//膨胀
dilate(Mat src, Mat output, Mat structElement,Point(-1,-1));
//腐蚀
erode(Mat src, Mat output,Mat structElement,Point(-1,-1));
//open
morphologyEx(Mat src, Mat output,MORPH_OPEN,Mat structElement);
//close
morphologyEx(Mat src, Mat output,MORPH_CLOSE,Mat structElement);
//tophat
morphologyEx(Mat src, Mat output,MORPH_TOPHAT,Mat structElement);
//blackhat
morphologyEx(Mat src, Mat output,MORPH_BLACKHAT,Mat structElement);
第四章 边缘/轮廓检测
阈值
一般可通过阈值操作来进行图像分割,从而获得各区域的轮廓,边缘,因此阈值放在第四章边缘轮廓检测中.
一般阈值
阈值操作threshold,设定高低阈值,像素在三个区间内分别采取不同措施对像素值进行修改
Mat src=imread(“path”);
Mat output;
src.copyTo(output);
threshold(Mat src,Mat output,default_value,max_value,type);
//threshold(src,dst,10,255,THRESHOLD_BINARY);
//THRESHOLD_BINARY 二值化,THRESHOLD_BINARY_INV 反二值化, THRESHOLD_TRUNC截断
自适应阈值
自适应阈值adaptiveThreshold, 只支持8bit图片
Mat src = imread(“path”);
Mat dst;
cvtColor(src,dst,COLOR_BGR2GRAY);
convertTo(dst,dst,CV_8UC1);
adaptiveThreshold(Mat src,Mat output, adaptiveMethod,threshold_type,int blockSize,double offset);
//adaptiveThreshold(src,output,ADAPTIVE_THRESH_MEAN_C,THRESHOLD_BINARY,15,-2);
//adaptiveMethod是自适应方法, threshold_type是阈值化类型,bloaksize是方形邻域块边长,offset是偏移量
轮廓检测
轮廓检测流程:
-
灰度处理
-
二值化,需要CV_8UC1类型的图片
-
检测轮廓
-
绘制轮廓
binary, contours, hierarchy = findContours(src, output,RETR_TREE,CHAIN_APPROX_NONE);
// findContours()会返回3个参数,第一个是二值化的图片,第二个是轮廓数据,第三个是层次结构,常用的是contours轮廓结构信息
//绘制轮廓
Mat res;
res = drawContours(src,contours,-1,Scalar(0,0,255),2);
// drawContours(Mat src, Mat contours,int contoursIdx,Scalar color,int thickness,int lineType,Mat hierarchy,int maxLevel,Point offset);
// contoursIdx = -1选择全部
// contours[i]可以选择轮廓
//常用boundingRect()函数, 作用:计算轮廓垂直边界最小矩形
Rect boundingRect(points);
//这里的points可以是Mat对象,也可是二维点集合,也可以是点序列
⚠️这里的output_contours类型是vector<vector<Point>>类型,实际上寸了x个轮廓,即vector<x>,每个轮廓是一系列的点集合,即vector<Point>, 想访问某个轮廓可以for循环vector<x>,得到的是vector<Point>
Mat src = (“path”);
Mat output_contours;
cv::findContours(Mat src, vector<vector<Point>>output_contours, int mode,int method, Point offset = Point());
/*
mode:轮廓检索类型
RETR_EXTRENAL,外面的轮廓,
RETR_LIST,所有轮廓,并保存到列表中,
RETR_CCOMP,所有轮廓,并组成两层,一个外轮廓一个内轮廓,
RETR_TREE,检测所有轮廓,并按层次分成树结构,这个比较常用
method:轮廓逼近方法
CHAIN_APPROX_NONE,直接画出轮廓,
CHAIN_APPROX_SIMPLE,只保留关键点,比如长方形是四个顶点
*/
边缘检测/提取
对于边缘提取算法,我们一般采用Sobel算子检测方法和Canny算法,Sobel算子对灰度渐变,多噪声图片处理效果很好,但对边缘定位精度较差,即定位不准确(大多数边缘不止一个像素); Canny算法不容易受到噪声干扰,利用强弱边缘,即弱边缘和强边缘相连时,才将弱边缘考虑,从而容易检测到真正的边缘,即定位精度比较高.
精度要求不高可以考虑Sobel边缘提取方法, 反之考虑Canny边缘提取方法,还熟悉Laplance算子,由于对噪声比较敏感,常不用在边缘检测上
Sobel边缘检测算法详解
-
Sobel算子分成横向检测了纵向检测,在解释前首先理解,边缘的概念,边缘理论上是像素值得突变处,导数正好是变化率的意义,因此在x轴方向导数最大,则是一个垂直边缘,相反在y轴方向变化最大,则是一个水平边缘,
-
通过计算像素的差值,得到的图像,越明亮的地方越是边缘(这也表示,并不是存在差值就是边缘点,而是比较明亮的才是,实现手段就是利用阈值进行过滤),将xy方向的sobel输出图进行线性混合,则得到了整个图像的边缘信息
实现方法:
- 最后,根据阈值T,若G(row,col)大于T,则认为是边缘点,过滤后得到最后的G,即图片边缘信息图(这里是单阈值)
- 理论使用G=sqrt(Gx^2 + Gy^2),实际考虑效率使用|G| = |Gx| + |Gy|,即计算各方向边缘图的绝对值,然后进行线性混合
- 其次计算y方向的边缘信息,得到Gy
- 首先计算x方向的边缘信息,得到Gx
- 灰度处理
- 得到图片后,先对图片进行增强,利用GaussianBlur过滤噪点
Mat src = imread(“path”);
Mat gray;
Mat output_x;
Mat output_y;
Mat output_absx;
Mat output_absy;
Mat sobel_image;
Mat output_laplance;
cvtColor(src, gray, COLOR_BGR2GRAY);
//Sobel(Mat src, Mat output, int depth, int dx, int dy,int ksize,double scale,double delta,int borderType);
//Sobel边缘检测算法实现基于cv::Sobel
Sobel(src,output_x,src.depth(),1,0,3); # 一般只用到前几个参数,dx和dy选择哪个方向就置1
convertScaleAbs(output_x,output_absx);
Sobel(src,output_y,src.depth(),0,1,3);
convertScaleAbs(output_y,output_absy);
addWeighted(output_absx,0.5,output_absy,0.5,sobel_image);
Mat k_sobel_x = (Mat_<double>(3,3)<< -1, 0, 1,
-2, 0, 2,
-1, 0, 1);
Mat k_sobel_y = (Mat_<double>(3,3)<< -1, -2, -1,
0, 0, 0,
1, 2, 1);
//x和y的滑动结果需要分别取绝对再相加,就得到了xy两方向的像素导数值,及时调用
//绝对值api
convertScaleAbs(
Mat k_laplance =(Mat_<double>(3,3)<< 0, -1, 0,
-1, 4, -1,
0 , -1, 0);
filter2D(Mat src, Mat output,src.depth(),Mat kernel);
Canny边缘检测算法
流程如下
-
GaussianBlur去噪点
-
灰度处理
-
计算Sobel梯度信息
-
NMS(非最大值抑制)得到疑似边缘点
-
简单说明,就是寻找像素点局部最大值,将非极大值点对应的灰度值设置为背景像素点而非边缘像素点, 像素点邻域内满足梯度值最大的点则判为边缘像素点,
-
上述操作完成了对非极大值点的抑制,提出了大部分非边缘点
-
此时得到的像素点是疑似边缘点
-
-
双阈值Double-Treshold来检测且定位真正的边缘和潜在的边缘,即强弱边缘
-
疑似边缘点高于高阈值,则保留
-
疑似边缘点低于低阈值,则排除
-
疑似边缘点在高低阈值中间时,则判断该像素是否连接到一个保留像素点, 连接则保留,反之排除
-
//cv::Canny()
Mat src =imread(“path”);
Mat src_gray;
Mat output;
GaussianBlur(src,src,Size(3,3),0,0,BORDER_DEFAULT);
cvtColor(src, src_gray, COLOR_BGR2GRAY);
int T1 = 40;
int T2 =190;
Canny(src_gray,output,T1,T2,3,true);
//Canny(Mat src, Mat output, double threshold1, thrshold2,int kernelSize, bool L2gradient=false)
//L2gradient=false时则用L1归一化,ture则用L2
第五章 模板匹配
模板匹配
左边是模板,右边是图片,模板在图像上滑动,从而寻找模板在图片中的位置
然后两图片逐像素比较,有点暴力匹配了,就是比较像素差异,一般的算法是相减然后平方,得到的数越小,像素点之间的相似程度越高,
这里有很多不同的方法template match modes
-
TM_SQDIFF 方差匹配
-
TM_SQDIFF_NORMED 归一化方差匹配
-
TM_CCORR 相关性匹配
-
TM_CCORR_NORMED 归一化相关性匹配
-
TM_CCOEFF 相关系数匹配
-
TM_CCOEFF_NORMED 归一化相关系数匹配
这里注意一下, 方差匹配法用最小值来表示最佳匹配,而相关性和相关系数两个方法用最大值表示最佳匹配
归一化是用来把匹配结果数据进行归一化,使其显示在0-1,可以理解为0-255,这样你可以直观看到结果图片的亮度,
/*
cv::matchTemplate(Mat src, Mat template,Mat result,int method,Mat mask);
如果你的图片是WxH,模板是wxh,那么你会得到一个(W-w+1)*(H-h+1)的图片,mask不是必须参数
这里的result就是得到值,也是个Mat对象,这里的元素都是相关的匹配方法的计算结果数值, 你可以通过minMaxLoc()来定位出最小值(我们这里用方差匹配),和最小值所在的位置,这个位置就是图片中位置,就是滑动时,模板在图像中的最上角的像素点,
也就是说,你得到的result通过minMaxLoc()可以得到方法匹配数值结果和模板在图片中左上角的像素点坐标,下一步你可以根据这个信息,来画个框,框出你的匹配结果
*/
int main() {
Mat tmp = imread("C:/Users/sherlock/Pictures/template.jpg");
Mat src = imread("C:/Users/sherlock/Pictures/test.jpg");
Mat result;
Mat dst;
src.copyTo(dst);
Point minLoc;
double minVal;
matchTemplate(src, tmp, result, TM_SQDIFF_NORMED);
cout << result.size() << endl;
minMaxLoc(result, &minVal, 0, &minLoc, 0);
Point result_rect = minLoc;
Size result_size = tmp.size();
rectangle(dst, Rect(minLoc, tmp.size()), Scalar(0, 100, 255), 1, 8, 0);
cout << minLoc << endl;
cout << minVal << endl;
imshow("tmp", tmp);
imshow("src", src);
imshow("result", result);
imshow("drawResult", dst);
waitKey(0);
return 0;
}
最大值最小值定位函数cv::minMalLoc();
cv::minMaxLoc(Mat src, double* minVal, double* maxVal, Point* minLoc, Point* maxLoc);
必须是8bit channels=1
Mat src = (Mat_<uchar>(2,2)<<28,21,12,3);
src.convertTo(src,CV_8UC1);
Point minLoc,maxLoc;
double minVal,maxVal;
minMaxLoc(src,&minVal,&maxVal,&minLoc,&maxLoc);
cout<<src<<endl;
cout<<minLoc<<endl;
cout<<minVal<<endl;
第六章 目标追踪
目标追踪算法apihttps://docs.opencv.org/3.4.10/d9/df8/group__tracking.html
tracking是基于视觉的目标跟踪扩展库,在opencv_contrib模块中
在tracking模块中,包含了很多目标跟踪的class,例如:
cv::Detector
cv::MultiTracker
cv::Tracker
cv::TrackerKCF //显然这是cv::Tracker的子类,继承了Tracker的cen
cv::TrackerMIL
首先要包含tracker头文件
#include <opencv2/tracking/tracker.hpp>
创建追踪器(由于创建的是个指针类,所以采用->符号来访问类函数(方法))
Ptr<TrackerMIL> tracker= TrackerMIL::create();
Ptr<TrackerTLD> tracker= TrackerTLD::create();
Ptr<TrackerKCF> tracker = TrackerKCF::create();
Ptr<TrackerMedianFlow> tracker = TrackerMedianFlow::create();
Ptr<TrackerBoosting> tracker= TrackerBoosting::create();
tracker->init(frame,bbox);
tracker->update(frame,bbox);
多目标追踪器
MultiTracker multi_trackers;
multi_trackers.add();
multi_trakcers.getObjects();
multi_trackers.getObjects().size();
bool cv::MultiTracker::add(Ptr<Tracker> newTracker,InputArray image,const Rect2d &boundingBox);
//.add()方法返回一个bool变量,表示成功与否,这里是添加一个追踪器和感兴趣box
bool cv::MultiTracker::add(vectro<Ptr<Tracker>> newTrackers, InputArray image, vector<Rect2d> boundingBoxes);
//.add()方法的重载是添加一组追踪器和对应感兴趣boxes
const vector<Rect2d> & cv::MultiTracker::getObjects() const;
//.getObjects()方法是返回MultiTracker类对象的objects成员,这个成员就是存储了全部的bboxes,是vector<Rect2d>类型,就是返回了个vector对象
//所以可以通过.size()方法来进行.getObjects().size()获得bboxes总个数,即vector总长度
//也可以通过[]访问某个bbox,即.getObjects9()[k]访问第k个bbox,得到的是个Rect类对象
bool::cv::MultiTracker::update(InputArray image)
//更新跟踪器状态,更新结果保存在MUltiTracker对象内部的跟踪器成员中
bool::cv::MultiTracker::update(InputArray image,std::vector<Rect2d> &boundingBoxes)
//更新一组跟踪器状态,这是必须要进行的操作,否则跟踪器会不知道当前目标的位置
选择ROI区域,并获得包围框
void selectROIs(String &windowName, InputArray image,DetectionBasedTracker::TrackedObject::PositionsVector &boundindBoxes, bool showCrosshair = true, bool fromCenter = false);
//这是获得多个roi框,你需要vector<Rect2d> bboxes;⚠️没有返回值
Rect selectROI(String &windowName, InputArray image, bool showCrosshair = true, bool fromCenter = false);
//获得一个roi框,你需要一个Rect bbox;⚠️返回的是该Rect框
第七章 其他
时间函数
// 利用getTickCount()
// 利用getTickFrequency()
// 时间计算逻辑是:
// 获得操作系统启动到当前的计时周期数, 获得CPU频率,单位是 重复次数/秒
// 根据周期数差值/CPU频率,可以得到秒数
int start_time = getTickCount();
for (int i=0;i<100;i++){
cout<<“nothing”<<endl;
}
int end_time = (getTickCount()-start_time)/getTickFrequency();
cout<<end_time<<“s”<<endl;