最近很多朋友问我如何去追踪一个乒乓球,然后利用PID算法来保证活动板的平衡,于是我利用树莓派和arduino实现了这个小实验,本文提出一种基于图像的圆形目标实时跟踪方法,用以解决圆形目标由远及近运动时跟踪稳定性不高的问题。然后将球体的中心坐标通过串口送给电机,利用电机来控制活动板的平衡。前篇博客我已经很好的讲解过了camshif原理和代码了,camshift代码可直接见上篇博客。霍夫变换检测圆的代码,并返回中心坐标和半径的代码我先贴出来吧。

#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{  
  bool flag=1;
   VideoCapture cap(0);
   if (!cap.isOpened())
    return -1;
    Mat frame,midimage,dst;
    while(flag)
    {
      cap>>frame;
      cvtColor(frame,midimage,CV_BGR2GRAY);
      blur(midimage,midimage,Size(9,9));
      //threshold(midimage,dst,96,255,THRESH_BINARY);
      vector<Vec3f>circles;
      HoughCircles(midimage,circles,CV_HOUGH_GRADIENT,1.2,10,200,100,0,0);
      for(size_t i=0;i<circles.size();i++)
      {
        Point center(cvRound(circles[i][0]),cvRound(circles[i][1]));
        int radius=cvRound(circles[i][2]);
        circle(frame,center,3,Scalar(255,255,255),-1,8,0);
        circle(frame,center,radius,Scalar(255,255,255),3,8,0);
        cout << cvRound(circles[i][0]) << "\t" << cvRound(circles[i][1]) << "\t"   
             << cvRound(circles[i][2]) << endl;
      }
    imshow("xiaorun",frame);
    if(waitKey(20)>=0)
        break;
         }
    return 0;

}

   计算机视觉应用中,目标跟踪一直是研究的热点内容之一。而在目标跟踪研究中,最值得关心的就是跟踪算法的实时性和准确性。为此,相关学者提出了很多不同的方法,其中应用最为广泛的是Gary R Bradski提出的基于自适应均值漂移(Continuously adaptive mean shift,CamShift)算法。CamShift算法依靠视频图像的色彩信息来实现对目标的跟踪,其运算速度快且比较适合不太复杂的背景下的目标跟踪,因此适合室内移动机器人对目标的跟踪。
  而对于基于视觉的室内移动机器人目标跟踪,仅仅依靠CamShift这种依靠颜色模型的跟踪方法,无法在照度不均的情况下获得圆形目标物的精确位置信息。而随机Hough圆变换具有对圆形目标识别精度高且速度快的特点。为此,本文提出采用CamShift算法与随机Hough变换结合的方法以实现对球形目标的快速、准确跟踪。
  1 算法原理
  1.1 CamShift算法介绍
  CamShift算法实际上是连续自适应的MeanShift算法的简称。该算法降低了由光照亮度变化对跟踪效果的影响,并且利用基于像素颜色概率分布信息进行目标跟踪的方法,使算法的效率比较高。CamShift算法采用基于概率密度的梯度攀升来寻找局部最优的思想对每一帧图像运用MeanShift算法做处理。根据前一帧图像的搜索结果自适应调整搜索窗的大小,从而实现对当前图像中目标的定位。
  算法主要步骤如下。
  1)首先将当前帧图像由RGB空间转换到HSV空间,并将当前图像整幅都作为搜索范围。
  2)设定搜索窗口(SearchWindow)大小与位置的初始值并计算窗口内的色度(Hue)分量概率分布直方图。
  3)对当前图像的每一个像素采用其颜色的概率值进行替换,从而得到颜色概率分布图。
  4)在颜色概率分布图中选取搜索窗。
  5)计算零阶矩:
  计算一阶矩:
  计算搜索窗的质心:
  6)调整搜索窗大小,窗口宽度为;长度为1.2w。
  7)将SearchWindow的中心移动到质心位置,当移动距离超过了设定的阈值时,则跳转到步骤5),当SearchWindow的中心与质心间的距离小于一个预设值或者迭代次数达到最大时,停止计算。
  8)在下一帧输入的图像中用上一步中的结果再一次计算搜索窗口的参数,然后跳转到步骤2)继续目标搜索。
  1.2 随机Hough圆变换介绍
  Hough变换是一种在图像中寻找直线、圆以及其他简单形状的方法。其基本思想是将原始图像中的曲线或者直线检测问题转变成在参数空间中聚类求峰值问题。由于圆形包含3个自由参数,需要在三维空间中投票求峰值,其计算量很大且对内存消耗大,算法运算时间长。因此XU等人提出了随机Hough变换(Randomized Hough Transform,RHT),使用了3个新的操作机制,即在图像空间中的随机抽样、参数空间中的动态链接列表以及连接图像空间和参数空间的收敛映射,从而加快了运算速度和提高了内存的利用率。然而随机采样会引入大量的无效累积和无效采样,在处理复杂背景和较大图像时对算法识别性能有较大的影响。因此有学者提出了改进的RHT算法,但不可否认的是在处理背景简单尤其是小图像的识别时,随机Hough变换运算速度很快,识别精度高。
  1.3 基于CamShift与RHT的球体跟踪方法
  由于CamShift算法是运用物体颜色信息和聚类的方式跟踪视频序列中的运动目标,因此算法具有较高的执行效率。而在双目移动机器人运用视差法计算球体目标三维坐标时,需要输入球体在图像中的二维坐标位置,二维坐标位置的误差过大会影响目标的定位及机器人姿态的调整。但是仅仅依靠CamShift算法返回的搜索区域的质心无法较精确的识别圆形目标的圆心及半径,考虑针对小图像运用随机Hough圆变换的具有高识别精度和运算速度快的特点,因此将CamShift和RHT两种算法结合并运用在简单的室内环境下对球形目标的跟踪上。在前文阐述的CamShift算法步骤中,步骤5)、6)、7)其实就是MeanShift算法的主要步骤,则CamShift与RHT结合的球体跟踪识别方法主要步骤表述如下。
  1)确定初始目标的位置及区域并计算目标的H分量直方图。
  2)利用直方图计算反向投影图。
  3)采用MeanShift算法在上一步的反向投影图中进行迭代搜索,当计算结果收敛或达到最大迭代次数时停止计算。
  4)利用MeanShift算法返回的搜索窗口的中心位置,取输入帧图像在周围的局部图像保存为子图像(SubImage),子图像的高度(subheight)和宽度度(subwidth)与搜索窗的宽度和长度相等。
  采用随机Hough圆变换对SubImage进行圆形特征检测。
  在子图像中若识别出圆形特征则返回其在原始图像当中的位置,假设子图像中圆心坐标为,原始图像中的圆心位置由如下坐标变换公式求得:
  5)将步骤3)中获得的新的搜索窗口参数带入到步骤2)中,继续下一帧图像的目标位置搜索。
  贴出利用camshift和霍夫变换的图片球体检测的代码吧,自行copy吧

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/core/core.hpp"
#include "opencv2/video/tracking.hpp"
#include <tuple>

using namespace cv;
using namespace std;

//This is used to obtain a window inside the image,
//and cut it around the image borders.
Rect GetROI(Size const imageSize, Point const center, int windowSize)
{
    Point topLeft (center.x - windowSize / 2, center.y - windowSize / 2);

     if (topLeft.x + windowSize > imageSize.width || topLeft.y + windowSize >
         imageSize.height)
    {
         windowSize = 
         min(imageSize.width - topLeft.x, imageSize.height - topLeft.y);
    } 
    return Rect(topLeft.x, topLeft.y, windowSize, windowSize);
}

//This is used to find pixels that likely belong to the circular object
//we wish to track.
Mat HistogramBackProjectionForTracking(Mat const& image, Rect const window)
{
     const int sizes[] = {256,256,256};
     float rRange[] = {0,255};
     float gRange[] = {0,255};
     float bRange[] = {0,255};
     const float *ranges[] = {rRange,gRange,bRange};
     const int channels[] = {0, 1, 2};

     Mat roi = image(window);

     Mat hist;
     if (image.channels() == 3)
      calcHist(&roi, 1, channels, Mat(), hist, 3, sizes, ranges);
     else
      calcHist(&roi, 1, &channels[0], Mat(), hist, 1, &sizes[0], &ranges[0]);

     Mat backproj;
     calcBackProject(&image, 1, channels, hist, backproj, ranges);
     return backproj;
}

//Return a new circle by using CAMSHIFT to track the object around the initial circle.
tuple<Point, int> HoughShift(Point const center, int const radius, Mat const& image)
{
    Mat backproj = HistogramBackProjectionForTracking(image, 
    GetROI(image.size(),center, radius));

     //Fill holes:
     cv::dilate(backproj, backproj, cv::Mat(), cv::Point(-1,-1));
     cv::dilate(backproj, backproj, cv::Mat(), cv::Point(-1,-1));

    const int windowTrackingSize = 4 * radius;
    RotatedRect track = CamShift(backproj, GetROI(image.size(), center,
    TermCriteria( CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1 ));

    return make_tuple(track.center, (track.size.width + track.size.height )/ 4);
}

int main(int argc, char** argv)
{
     Mat image = cv::imread("image.jpg");

     Mat before, after; image.copyTo(before); image.copyTo(after);
     Mat gray; cv::cvtColor(image, gray, CV_BGR2GRAY);

     std::vector<cv::Vec3f> circles;
     HoughCircles(gray, circles, CV_HOUGH_GRADIENT, 2, gray.cols / 3, 20, 40,
     gray.cols / 20, gray.cols / 5);

     for (int i = 0; i < circles.size(); ++i)
     {
         auto circle = HoughShift(Point(circles[i][0], circles[i][1]), 
         circles[i][2], image);

         circle(before, Point(circles[i][0], circles[i][1]), circles[i][2], 
         Scalar(128, 128, 30),  2);
         circle(after, get<0>(circle), get<1>(circle), Scalar(255, 0 , 0), 2);
     }

     imshow("Initial Circles", before);
     imshow("Refined Circles", after);
     waitKey(-1);

     return 0;
}