在前面的报告中我们实现了用SURF算法计算目标在移动摄像机拍摄到的视频中的位置。由于摄像机本身像素的限制,加之算法处理时间会随着图像质量的提高而提高,实际实验发现在背景复杂的情况下,结果偏差可能会很大。
本次改进是预备在原先检测到的特征点上加上某种限制条件,以提高准确率。
问题:如何判定检测到的特征点是否是我们需要的点(也就是目标区域上的点)?
可行方案:用形态学找出目标的大致区域,然后对特征点判定。
特征点(SURF算法或者其他的算法)已有,我们来一步步实现找到目标大致区域。
下图假设为视频中的某一帧
我们要在这一帧中找出“停”字的大致区域(“停”的颜色和背景颜色可以酌情设置,并且和少量代码相关,可以修改)。
目标被设置成了两种颜色(后面的操作也是基于两个通道R和B),原因是一种颜色太简单以致不好分离通道然后计算,三种颜色往上也没有必要,因为我们只计算两通道,多了会增加计算时间。(其他情况留作进一步的讨论)
1、分离通道
Mat img_scene = imread("image1.jpg"); //读取图像 resize(img_scene,img_scene,cvSize(0,0),0.2,0.2); //把图像调整到合适的大小 vector<Mat> channels; split(img_scene, channels); // 分离色彩通道, 把一个3通道图像转换成3个单通道图像 Mat img_scene_BlueChannel = channels.at(0); // 红通道 Mat img_scene_GreenChannel = channels.at(1); // 绿通道 Mat img_scene_RedChannel = channels.at(2); // 蓝通道
结果如下图:
红色通道
蓝色通道
我们看到原图的红色部分在red channel中位深色,蓝色部分在blue channel中为深色,这一步我们分离了目标的两个部分,两幅图得背景看起来差别不大。
2、对图像进行二值化
首先定义阈值
#define threshold_value_red 150 // 红色通道阈值 #define threshold_value_blue 160 // 蓝色通道阈值
二值化处理
Mat Seg_img_red; // 红色通道阈值分割结果图 Mat Seg_img_blue; // 蓝色通道阈值分割结果图 threshold(img_scene_RedChannel, Seg_img_red, threshold_value_red, 255, THRESH_BINARY); // 红色通道进行阈值分割(大于阈值时候取255) threshold(img_scene_BlueChannel, Seg_img_blue, threshold_value_blue, 255, THRESH_BINARY); // 蓝色通道进行阈值分割(小于阈值时候取255) imshow("Seg_img_red",Seg_img_red); imshow("Seg_img_blue",Seg_img_blue);
处理结果
二值化后图像简单化了,去除了冗余,值得高兴的是目标看起来更突出了,目标区域似乎能够检测出来,但是背景也有大片干扰,下面就是用两个通道的好处了。
3、合并图像
这里我们注意到red channel中“停”字为黑色,背景是白色,而blue channel中刚好相反,于是想到试着对两幅图进行异或运算
Mat Object_img; bitwise_xor(Seg_img_red, Seg_img_blue, Object_img); // 求两个图像的交集获取目标的潜在颜色区域 imshow("Object_img",Object_img);
结果
好的,看起来更好了,虽然背景还是有干扰(这也是无法避免的),目前我们已经使用上了两个通道的信息,只能继续往下寻找其他方法。
4、腐蚀图像
图像中有不少像胡椒粉一样的噪声,先用腐蚀去掉
erode(Object_img,Object_img,cv::Mat()); imshow("Object_img_erode",Object_img);
结果
清爽不少
5、计算连通域
vector<vector<Point> > contours; findContours(Object_img, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); Mat result(Object_img.size(), CV_8U, Scalar(0)); drawContours(result, contours, //画出轮廓 -1, // 画出所有的轮廓 Scalar(255), // 用白线画出 2); // 轮廓线的粗细为2 namedWindow("Contours"); imshow("Contours", result); // 显示图像中所有的连通域轮廓
处理结果
有很多小的连通域,可以放心的先去掉
6、连通域去噪
方法一:
// 去除图像中的连通域噪声 int cmin = 50; // 最小的轮廓长度 int cmax = 1500; // 最大的轮廓长度 vector<vector<Point>>::const_iterator itc = contours.begin(); while (itc != contours.end()) { if (itc->size() < cmin || itc->size() > cmax) itc = contours.erase(itc); // 删除当前连通域轮廓 else ++itc; } // 画出去掉连通域噪声后的连通域 Mat original(Object_img.size(), CV_8U, Scalar(0)); Mat result_hull(Object_img.size(), CV_8U, Scalar(0)); Mat threshold_output(Object_img.size(), CV_8U, Scalar(0)); drawContours(original, contours, -1, // 画出所有的轮廓 Scalar(255), // 用白线画出 1); // 轮廓线的粗度为2 namedWindow("Contours noise reduced"); imshow("Contours noise reduce", original); // 画出去掉连通域噪声后的连通域
处理结果(轮廓长度参数表示我们要选取适当的连通域,将范围缩小可以在单张图片中取得更好的结果,例如将cmin=400直接就有
但是为了在视频中处理要将范围适当放宽),结果如下:
看起来离目标不远了。
方法二:
方法一使用了人为设置的参数,但在视频处理中要尽量避免这种做法,方法二将得到的连通域按边界像素数量进行排序,然后选取边界像素数量最大的contours.size()*1/n数量的连通域(n按实际帧大小和摄像机距离目标距离等因素选取适当的值)。
int *ca = new int[contours.size()]; //定义连通域边界像素排序数组 itContours = contours.begin(); for (int i = 0;i < contours.size(), itContours != contours.end();++i, ++itContours) { ca[i] = itContours->size(); } sort(ca, ca + contours.size()); //按连通域边界像素的多少排序 int s = contours.size()/10; int threshhold_con = ca[contours.size() - s]; vector<vector<Point>>::const_iterator itc = contours.begin(); while (itc != contours.end()) { if (itc->size() < threshhold_con) itc = contours.erase(itc); // 删除当前连通域轮廓 else ++itc; }
方法二结果
看起来似乎没什么差别,算法复杂了一点,但为了最后在视频中处理,我们选用了第二种方法。
7、计算连通域的凸包络、填充包络
Mat result_hull(Object_img.size(), CV_8U, Scalar(0)); Mat threshold_output(Object_img.size(), CV_8U, Scalar(0)); /// 对每个轮廓计算其凸包 vector<vector<Point> >hull( contours.size() ); for( int i = 0; i < contours.size(); i++ ) { convexHull( Mat(contours[i]), hull[i], false ); } /// 绘出轮廓及其凸包 Mat drawing = Mat::zeros( threshold_output.size(), CV_8UC3 ); for( int i = 0; i< contours.size(); i++ ) { Scalar color = Scalar( rng.uniform(0, 255), rng.uniform(0,255), rng.uniform(0,255) ); drawContours( drawing, contours, i, color, 1, 8, vector<Vec4i>(), 0, Point() ); drawContours( drawing, hull, i, color, 1, 8, vector<Vec4i>(), 0, Point() ); fillConvexPoly(result_hull,&hull[i][0],hull[i].size(),Scalar(255,0,0)); }
处理结果
实际上到这里,如果我们的把落入上图白色区域的特征点认为是正确的点已经能够减少一部分误差了,但本着精益求精的精神,还是应该再好一点。
8、计算连通域的外接矩形
这一步为可选项,可以看到前面一张图中的目标是近似正方形的,实际我们所用的目标就是正方形的,那么在摄像机离目标中心法线偏离角度不是太离谱的情况,得到的目标图像的外接矩形的长宽比的变化可以表示为一个固定的范围,这里思路是,计算所有连通域外接矩形长宽比(用短的比长的),得到一组0~1的浮点数,排序后和第七步一样选取适当的百分比,筛选长宽比靠近1的外接矩形和对应的连通域,代码如下:
Rect *r = new Rect[contours.size()];//定义外接矩形数组 double *ra = new double[contours.size()]; //定义外接矩形长宽比数组 double *rb = new double[contours.size()]; Mat obj_rec = Mat::zeros( threshold_output.size(), CV_8UC3 ); for( int i = 0; i < contours.size(); i++ ) { convexHull( Mat(contours[i]), hull[i], false ); r[i] = boundingRect(Mat(contours[i]));//boundingRect获取这个外接矩形; rb[i] = ra[i] = rate(r[i].width,r[i].height); //计算长宽比 rectangle(obj_rec, r[i], Scalar(255), 2); } sort(ra,ra+contours.size()); //将外接矩形长宽比排序 int k = contours.size()/3; double threshhold_rate = ra[contours.size() - k]; //定义外接矩形长宽比阈值 //绘制通过长宽比阈值限制后的外接矩形 Mat obj_rec_thr = Mat::zeros( threshold_output.size(), CV_8UC3 ); itContours = contours.begin(); for( int i = 0; i < contours.size(), itContours != contours.end(); ++i) { if(rb[i]>threshhold_rate) { rectangle(obj_rec_thr, r[i], Scalar(255,0,0), 2); ++itContours; } else itContours = contours.erase(itContours); }
筛选前外接矩形:
筛选后连通域外接矩形:
相应的连通域图:
只剩下一点噪声了。
9、去掉最后的噪声
第三步中合并图像我们使用的是异或(xor),也就是同一个位置像素在两个通道中值相同和不同这两种结果在目标图像中以黑和白区分开来了,我们这个例子中目标图像白色区域是为两个通道相同像素点值不同的结果。也就是说,前一张图中的两块连通域中的像素在第二步阈值处理后的两张图中相应位置的像素值刚好不同。于是我们将得到的两个连通域的外接矩形设置为兴趣区,我们得到两个兴趣区,以第一个兴趣区的位置和大小在阈值处理后的图像上分别截取对应的区域(做这步我们要将
threshold(img_scene_RedChannel, Seg_img_red, threshold_value_red, 255, THRESH_BINARY); // 红色通道进行阈值分割(大于阈值时候取255) threshold(img_scene_BlueChannel, Seg_img_blue, threshold_value_blue, 255, THRESH_BINARY); // 蓝色通道进行阈值分割(小于阈值时候取255)
中的255改为1,方便累加处理),又得到两块区域,分别求像素和sum_red,sum_blue,求比值(小的比大的),我们得目标区域是既有蓝色的又有红色的(比值可通过自己设计目标图形和颜色修改),噪声区域是不一定的,我们只保留比值大于0.2的,太小的就不要了。
实现代码
for( int i = 0; i < contours.size(), itContours != contours.end(); ++i) { if(rb[i]>threshhold_rate) { rectangle(obj_rec_thr, r[i], Scalar(255,0,0), 2); // ++itContours; Mat imageROI_red = Seg_img_red(cv::Rect(r[i].x, r[i].y, r[i].width, r[i].height)); Mat imageROI_blue = Seg_img_blue(cv::Rect(r[i].x, r[i].y, r[i].width, r[i].height)); long long int sum_red = 0, sum_blue = 0; int nr=imageROI_red.rows; int nc=imageROI_red.cols; // outImage.create(image.size(),image.type()); if(imageROI_red.isContinuous()) { nr=1; nc=nc*imageROI_red.rows*imageROI_red.channels(); } for(int i=0;i<nr;i++) { const uchar* Data_red=imageROI_red.ptr<uchar>(i); const uchar* Data_blue=imageROI_blue.ptr<uchar>(i); // uchar* outData=outImage.ptr<uchar>(i); for(int j=0;j<nc;j++) { sum_red += *Data_red; sum_blue += *Data_blue; // *outData++=*inData++/div*div+div/2; } } double pixel_sum_rate = rate((double)sum_red, (double)sum_blue); cout << sum_red << "," << sum_blue << endl; cout << pixel_sum_rate << endl; if(pixel_sum_rate < 0.2) itContours = contours.erase(itContours); else ++itContours; imshow("imageROI_red", imageROI_red); imshow("imageROI_blue", imageROI_blue); } else itContours = contours.erase(itContours); }
代码是直接在第八步中插入的,结果如下:
通过输出中间值可看到
第一个外接矩形区域sum_red=0,sum_blue=420,比值为0;
第二个外接矩形区域sum_red=12317,sum_blue=9153,比值为0.743119;
10、结论
我们已经成功找到目标所在区域,实现了预期的效果,用这个区域去限制SURF特征点预计可以得到更加精确的结果。