You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化台球桌比赛区域的四边轮廓检测效果?

台球桌比赛区域四边形轮廓检测优化需求

我正在开展一项项目,需从视频帧中检测台球桌与台球,并重建2D游戏小地图,核心目标是获取台球桌比赛区域的边缘。目前采用的流程为:种子生长算法生成比赛区域mask → 对mask执行形态学操作 → 轮廓检测 → approxPolyDP四边形拟合,但检测效果无法满足需求。我需要检测出的轮廓尽可能覆盖图像中的白色区域,甚至在部分场景中可超出白色区域,以准确估计比赛区域边缘;同时输入视频中的台球桌无固定颜色,需适配该特性。

相关实现代码如下:

比赛区域mask生成代码

Mat src = imread(src_path);
    assert(!src.empty());
    imshow("Source", src);
    
    Mat hsv;
    cvtColor(src, hsv, COLOR_BGR2HSV);
    imshow("HSV", hsv);
    
    /* 开始处理背景与比赛区域掩码 */
    
    Mat sharp;
    Mat sharpening_kernel = (Mat_<double>(3, 3) << -1, -1, -1,
            -1, 9, -1,
            -1, -1, -1);
    filter2D(hsv, sharp, -1, sharpening_kernel);
    //imshow("Sharpening", sharp);
    
    double sigma=1;
    GaussianBlur(sharp, sharp, Size(), sigma);
    //imshow("Blurred Sharp", sharp);
    
    //waitKey(0);
    
    int min_region_area = int(min_region_area_factor * sharp.cols * sharp.rows);  // 忽略小区域
    int max_region_area = int(max_region_area_factor * sharp.cols * sharp.rows);  // 忽略过大区域
    
    // dest用不同数值标记所有区域:0-未确定,255-忽略,其他-已确定
    uchar padding = 1;  // dest中用于标记区域的数值
    Mat dest = Mat::zeros(sharp.rows, sharp.cols, CV_8UC1);
    
    // mask记录当前区域,始终用1标记
    Mat mask = Mat::zeros(sharp.rows, sharp.cols, CV_8UC1);
    
    // 最终保存的掩码
    Mat final_mask;
    
    // 遍历整张图像,对未确定像素执行种子生长
    for (int x=0; x<sharp.cols; ++x) {
        for (int y=0; y<sharp.rows; ++y) {
            if (dest.at<uchar>(Point(x, y)) == 0) {
                grow(sharp, dest, mask, Point(x, y), thresh);

                int mask_area = (int)sum(mask).val[0];  // 计算种子生长得到的区域面积
                if (mask_area > min_region_area && mask_area < max_region_area) {
                    dest = dest + mask * padding;   // 将新区域记录到dest
                    final_mask = mask*255;
                    //waitKey();
                    if(++padding > max_region_num) { 
                        printf("run out of max_region_num..."); 
                        return -1; 
                    }
                } else {
                    dest = dest + mask * 255;   // 标记为忽略区域
                }
                mask = mask - mask;     // 重置mask,准备下一次种子生长
            }
        }
    }

种子生长函数代码

void grow(Mat& src, Mat& dest, Mat& mask, Point seed, int thresh) { //thresh=200
    /* 对给定种子执行种子生长
     * 参数:
     *   src: 源图像
     *   dest: 标记像素状态(已确定/未确定/忽略)的矩阵
     *   mask: 记录当前种子生长区域的矩阵
     */
    stack<Point> point_stack;
    point_stack.push(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-1
                || estimating_point.y < 0
                || estimating_point.y > src.rows-1) {
                // 估计点不能超出图像范围
                continue;
            } else {
                // delta = (H-H')² + (S-S')² + (V-V')²
                int delta = int(pow(src.at<Vec3b>(center)[0] - src.at<Vec3b>(estimating_point)[0], 2)
                                + pow(src.at<Vec3b>(center)[1] - src.at<Vec3b>(estimating_point)[1], 2)
                                + pow(src.at<Vec3b>(center)[2] - src.at<Vec3b>(estimating_point)[2], 2));
                if (dest.at<uchar>(estimating_point) == 0
                    && mask.at<uchar>(estimating_point) == 0
                    && delta < thresh) {
                    mask.at<uchar>(estimating_point) = 1;
                    point_stack.push(estimating_point);
                }
            }
        }
    }
}

轮廓拟合代码

Mat im = imread(argv[1], 0); // 边缘图像
Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(11, 11));
Mat morph;
morphologyEx(im, morph, MORPH_CLOSE, kernel);

int rectIdx = 0;
//vector<vector<Point>> contours;
//vector<Vec4i> hierarchy;
findContours(morph, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE, Point(0, 0));
for (size_t idx = 0; idx < contours.size(); idx++)
{
    RotatedRect rect = minAreaRect(contours[idx]);
    double areaRatio = abs(contourArea(contours[idx])) / (rect.size.width * rect.size.height);
    if (areaRatio > .99)
    {
        rectIdx = idx;
        break;
    }
}
//cout << contours.size() << ", rectIdx: " <<rectIdx<<", cont[idx] = "<<contours[rectIdx]<<endl;
// 获取轮廓的凸包
vector<Point> hull;
convexHull(contours[rectIdx], hull, false, true);

// 可视化
Mat rgb;
Mat only_contours = Mat::zeros(im.rows, im.cols, CV_8UC1);
cvtColor(im, rgb, COLOR_GRAY2RGB);
//drawContours(rgb, contours, rectIdx, Scalar(0, 0, 255), 2);
for(size_t i = 0; i < hull.size(); i++)
{
    line(rgb, hull[i], hull[(i + 1)%hull.size()], Scalar(0, 255, 0), 2);
    line(only_contours, hull[i], hull[(i + 1)%hull.size()], Scalar(255), 2);
}

imshow("Result", rgb);
imshow("Contour", only_contours);
imwrite("./contour.png", only_contours);

vector<Point> approx;
double d=0;
do
{
    d=d+0.5;
    approxPolyDP(contours[0],approx,d,true);
    //cout << approx.size() << " " <<d<<endl;
}
while (approx.size()>4);
cout << "Approx: " << approx << endl;
contours.push_back(approx);

drawContours(rgb,contours,contours.size()-1,Scalar(0, 0, 255),3);
imshow("Ctr",rgb);

优化方案

1. 种子生长阶段修正与优化

  • 颜色空间与相似度计算调整:放弃HSV全通道欧氏距离,改用Lab颜色空间(更贴合人眼感知,减少颜色偏差影响),或单独用V通道(亮度)结合S通道(饱和度)做判断——比赛区域通常是高亮度、低饱和度区域,可过滤掉彩色干扰。
  • 动态阈值替代固定值:不要用固定thresh=200,可根据种子点的局部颜色方差动态计算阈值,或分区域统计HSV分布设置自适应阈值,避免漏连有效区域或误连背景。
  • 种子点精准筛选:无需遍历所有像素,先通过Canny边缘检测提取潜在桌边缘,在边缘附近的高亮度区域选种子;或先做全局亮度阈值分割,缩小候选区域后再执行种子生长,减少无效计算。
  • 区域筛选逻辑修复:当前代码中final_mask会被每次符合条件的区域覆盖,最终仅保留最后一个区域,这是错误的。应收集所有符合面积范围的区域,选择面积最大的区域(台球桌比赛区域是画面中面积最大的区域之一)作为最终mask。

2. Mask后处理优化

  • 形态学操作动态适配:固定大小的椭圆核易导致过度膨胀或修复不足,可根据图像分辨率动态设置核大小(比如设为图像最短边的1/50);先做开运算去除小噪点,再做闭运算填补孔洞,避免边缘失真。
  • Mask补全修复:若mask存在孔洞或边缘残缺,可先用cv::convexHull生成凸包,再用cv::fillPoly补全;或用cv::inpaint修复缺失部分,确保mask尽可能覆盖完整比赛区域。

3. 轮廓拟合阶段优化

  • 轮廓筛选逻辑放宽与精准化:当前areaRatio > .99的阈值过于严格,台球桌mask不可能完全贴合最小外接矩形,建议将阈值降至0.8-0.9;同时优先筛选面积最大的轮廓,再结合轮廓凸包面积比、外接矩形宽高比(台球桌有标准比例,如斯诺克2:1、美式4:3)过滤无效轮廓。
  • approxPolyDP拟合优化:
    • 不要用contours[0]拟合,应使用筛选后的目标轮廓(如面积最大的轮廓)。
    • 放弃循环递增d的方式,改用OpenCV官方推荐的方法:以轮廓周长的百分比作为epsilon(如epsilon = 0.02 * cv::arcLength(contour, true)),拟合更稳定。
    • 若拟合后点数多于4,先对轮廓做凸包,再对凸包执行approxPolyDP——台球桌是凸四边形,凸包可过滤掉轮廓上的小凸起。
  • 强制四边形约束:若approxPolyDP无法得到4个点,可先用cv::minAreaRect获取旋转矩形,提取四个顶点作为初始四边形,再结合mask区域向外扩展若干像素,确保轮廓覆盖甚至超出mask边缘。

4. 整体流程重构(可选)

  • 先边缘检测再生成mask:先对原图做Canny边缘检测,再用霍夫直线检测提取潜在桌边缘直线,通过直线的交点、角度筛选出符合台球桌特征的四条边,再生成mask——这种方法对无固定颜色的场景鲁棒性更强。
  • 多帧融合跟踪:针对视频帧,可结合前后帧的检测结果,用卡尔曼滤波跟踪台球桌轮廓,减少单帧检测的误差。

内容的提问来源于stack exchange,提问作者LordGrim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 01:32:03