返回

台球桌边缘检测优化:区域生长、轮廓提取及自适应阈值

Ai

如何优化检测台球桌边缘的流程?

这篇文章要解决的问题,简单说,就是如何从视频帧里更精准地找出台球桌的边缘。 你已经做了初步尝试,但结果还不太满意,想知道怎样改进。

问题原因分析

从你提供的代码和结果图来看,主要问题出在几个方面:

  1. 区域生长算法(grow 函数)过于敏感: 容易受到颜色细微变化的影响, 将一些非台球桌区域(白色区域)包括进来。
  2. 轮廓提取后的处理不够完善: 获得的初始轮廓包含很多噪声,并且形状不规则。approxPolyDP 尝试逼近四边形,但容易受到轮廓噪声影响, 造成找到的矩形偏差大.
  3. 对于不同的图像,需要统一的处理逻辑: 给出的两个例子显示出很大的差异,对于图二甚至应该将外部作为有效区域进行检测, 而你提供的代码主要着眼于内部轮廓的检测与分析。

解决方案

针对以上问题,我们可以从以下几个方面入手改进:

1. 优化区域生长算法

核心思路是减少grow函数的敏感性,提升鲁棒性。

  • 原理: 不再直接比较原始像素值, 而是考虑颜色空间的差异,并增加对梯度的考量。
  • 改进 grow 函数:
void grow_improved(Mat& src, Mat& dest, Mat& mask, Point seed, int colorThresh, int gradThresh) {
    stack<Point> point_stack;
    point_stack.push(seed);

    Vec3b seedColor = src.at<Vec3b>(seed); // 获取种子点的颜色

    while(!point_stack.empty()) {
        Point center = point_stack.top();
        mask.at<uchar>(center) = 1;
        point_stack.pop();

        for (int i=0; i<8; ++i) {
            Point estimating_point = center + PointShift2D[i];
            if (estimating_point.x < 0
                || estimating_point.x >= src.cols
                || estimating_point.y < 0
                || estimating_point.y >= src.rows) {
                continue;
            }

            if (dest.at<uchar>(estimating_point) == 0 && mask.at<uchar>(estimating_point) == 0) {
                Vec3b currentColor = src.at<Vec3b>(estimating_point);
                // 计算颜色差异(例如,在HSV空间中)
                int colorDiff = colorDistance(seedColor, currentColor);

                // 计算梯度 (可选, 如果图像有明显边缘)
                int gradientMag = 0;
                 if (estimating_point.x > 0 && estimating_point.x < src.cols - 1) {
                     Vec3b colorLeft = src.at<Vec3b>(estimating_point - Point(1, 0));
                     Vec3b colorRight = src.at<Vec3b>(estimating_point + Point(1, 0));
                     gradientMag = colorDistance(colorLeft, colorRight);
                 }
              //   Mat grad_x, grad_y, abs_grad_x, abs_grad_y;

                if (colorDiff < colorThresh && gradientMag < gradThresh) {
                    mask.at<uchar>(estimating_point) = 1;
                    point_stack.push(estimating_point);
                    dest.at<uchar>(estimating_point) = 1; // 标记已处理
                } else{
                    dest.at<uchar>(estimating_point) = 255;
                }
            }
        }
    }
}

// 计算颜色距离 (示例, 可根据需要调整)
int colorDistance(Vec3b c1, Vec3b c2) {
   // 可以考虑转换到 HSV 或 Lab 颜色空间进行计算
    return sqrt(pow(c1[0] - c2[0], 2) + pow(c1[1] - c2[1], 2) + pow(c1[2] - c2[2], 2));
}

// 使用方法 (在原代码中替换 `grow` 调用)
// ...
 grow_improved(sharp, dest, mask, Point(x, y), 30, 50); // 调整颜色和梯度阈值
// ...

  • 颜色差异计算: 上面给的是 RGB 空间中的简单距离计算。建议尝试转到 HSV 或 Lab 颜色空间,,这样颜色差异的计算会更符合人眼感知. 可以自己编写 HSV/Lab 的颜色距离函数, 或直接用 OpenCV 的 cvtColor 转换色彩空间后计算。

  • 梯度计算: 上述例子中用左右像素简单计算梯度。如果需要更精细的梯度信息,可以考虑使用 Sobel 算子 (Sobel函数).

         //计算梯度的参考实现:
         Mat gray;
         cvtColor(src, gray, COLOR_BGR2GRAY); // 先转成灰度图
         Mat grad_x, grad_y;
         Sobel(gray, grad_x, CV_16S, 1, 0, 3);  // X方向梯度
        Sobel(gray, grad_y, CV_16S, 0, 1, 3);  // Y方向梯度
         // 计算梯度幅值
        convertScaleAbs(grad_x, grad_x);
        convertScaleAbs(grad_y, grad_y);
        Mat gradientMag;
         addWeighted(grad_x, 0.5, grad_y, 0.5, 0, gradientMag); // 简单融合
    
         // 在 grow_improved 中使用 gradientMag.at<uchar>(estimating_point) 进行比较
     ```
    
    
  • 安全建议:

    • colorThreshgradThresh 需要根据实际情况调整. 太小的值会导致区域生长过于严格,太大会导致区域生长过于宽松。 可以提供可配置的接口让用户调整,或者根据图像特征自动计算。
    • 为防止栈溢出 (如果处理的区域很大), 可以将递归的区域生长算法改为迭代版本. 上面的代码示例已经是迭代版本,因此这部分不用改动。

2. 优化轮廓提取和四边形逼近

这部分主要解决找到的轮廓不精确,以及 approxPolyDP 效果不佳的问题.

  • 原理: 使用更 robust 的边缘检测和轮廓筛选方法,并优化 approxPolyDP 的使用。

  • 具体步骤:

    1. Canny 边缘检测 (替代形态学操作): Canny 边缘检测对噪声的抑制更好,也更能找到清晰的边缘。

      Mat canny;
      Canny(morph, canny, 50, 150, 3); // 参数需要根据实际情况调整
      imshow("Canny", canny);
      
    2. 轮廓筛选: 移除面积过小或过于细长的轮廓。

      vector<vector<Point>> contours;
      vector<Vec4i> hierarchy;
      findContours(canny, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); // 只检测外轮廓
      
      vector<vector<Point>> filteredContours;
      for (size_t i = 0; i < contours.size(); i++) {
          double area = contourArea(contours[i]);
          Rect boundingBox = boundingRect(contours[i]);
          double aspectRatio = (double)boundingBox.width / boundingBox.height;
      
          // 筛选条件 (根据实际情况调整)
          if (area > 1000 && aspectRatio > 0.5 && aspectRatio < 2) { // 面积和长宽比
              filteredContours.push_back(contours[i]);
          }
      }
      
      // 可视化筛选后的轮廓
      Mat contourImage = Mat::zeros(canny.size(), CV_8UC3);
      drawContours(contourImage, filteredContours, -1, Scalar(0, 255, 0), 2);
      imshow("Filtered Contours", contourImage);
      
      
    3. 改进四边形逼近:

     vector<Point> bestApprox;
     double minPerimeter = 1e9; // 随便设置个很大的初始周长值。
     //对筛选后的所有轮廓都进行一次循环, 寻找最合适的一个.
      for (const auto& cnt : filteredContours) {
        double perimeter = arcLength(cnt, true);
        vector<Point> approx;
    
        //逐步增加精度, 找最符合条件的四边形, 限制周长和边数
       double d=0;
       do
        {
         d=d+0.5;
            approxPolyDP(cnt, approx, d, true); // 注意:这里用了原始的 cnt, 不是之前的 approx
    
        }while(approx.size()>4);
    
          if (approx.size() == 4 && perimeter < minPerimeter)
            {
                bestApprox = approx;
                 minPerimeter = perimeter;
           }
      }
    
     if (!bestApprox.empty())
     {
          // 绘制最终的四边形
            vector<vector<Point>> finalContour;
            finalContour.push_back(bestApprox);
          drawContours(rgb, finalContour, 0, Scalar(255, 0, 0), 3);
           imshow("Final Rectangle", rgb);
    
            //输出找到的点坐标。
          cout << "Best Approx: " << bestApprox << endl;
    
     } else {
         cout << "未找到合适的四边形。" << endl;
    
      }
    
    • 主要改动是用 arcLength 计算周长作为约束,这样找到的四边形会更接近真实的台球桌。
    • 上面是迭代对所有符合要求的轮廓进行遍历,这样可以找到更符合要求的四边形。

3. 统一处理不同图像:自适应阈值与参数

解决不同光照、不同颜色台球桌的适应性问题。

  • 原理: 使用图像的统计信息,自动调整阈值等参数。

  • 实现:

    1. 颜色阈值自适应: 可以根据种子点的颜色,动态调整 grow_improved 中的 colorThresh。 例如,将 colorThresh 设置为种子点颜色在 HSV 空间中饱和度和亮度的某个比例。

    2. 获取自适应参数:

     //  Mat hsv;
      //cvtColor(src, hsv, COLOR_BGR2HSV); //将颜色转化到 HSV 空间进行操作会更加符合人眼对颜色的感知
    
       // 获取种子像素的颜色。
       Vec3b seedColor = sharp.at<Vec3b>(seed);
    
      // 可以在HSV空间中分别计算均值和方差, 更能反映出整体情况.
      //Scalar mean, stddev;
     // meanStdDev(hsv, mean, stddev);
    
       // 根据图像的统计信息,动态调整 colorThresh 和 gradThresh
      // double colorThresh = 0.1 * stddev.val[1]; // 例如,使用饱和度的标准差的一部分。 值需要调
     //  double gradThresh = ... ;
    
     double  colorThresh=75;
    
    1. 梯度阈值调整 : 在有光照变化或阴影时特别有用, 可以使用统计直方图来完成自适应梯度调整, 让梯度的区分更加合理。

    2. Canny 边缘检测阈值自适应: OpenCV 有提供 adaptiveThreshold 函数. 可以在调用 Canny 前,使用它来自适应调整。 但针对台球桌场景,使用大津法 (Otsu's method) 可能会更合适。

    ```c++
     Mat gray;
     cvtColor(morph, gray, COLOR_BGR2GRAY);
    double otsuThreshold = threshold(gray, Mat(), 0, 255, THRESH_BINARY | THRESH_OTSU); // 获取大津法阈值。
    

    Canny(morph, canny, otsuThreshold * 0.5, otsuThreshold, 3); // 用大津法得到的阈值来调整。

    ```
    

总结

通过上述的优化方案,我们可以得到一个比较通用,而且相对精确的台球桌边缘检测方案。 主要包括:

  1. 使用改进的区域生长算法,更加精确地找到台球桌所在的区域。
  2. 使用 Canny 边缘检测,并配合适当的轮廓筛选,来进一步处理,找到可靠轮廓。
  3. 使用改进的四边形逼近方法。 限制了边数为 4, 通过调整参数得到最优解.
  4. 使用一些简单的自适应策略,提高算法的鲁棒性。

把这些改动组合起来,你应该可以获得比之前好得多的结果. 如果对边缘的平滑度有更高要求,还可以做进一步的曲线拟合等优化。